darknetでマルチクラス学習と画像認識

今までtensorflowなどでYoLoV2をしてきましたが、今回は複数の物体判別ができるようにdarknetで多クラスの学習をしようと思います。

参考にしたのは以下の2つのHPです。貴重な情報ありがとうございます。

 

追記(2018_04)

yolov3 の学習はこちら
http://weekendproject9.hatenablog.com/entry/2018/04/30/205622

・環境

 ubuntu16.04
 gtx-1070
 python 2.7
 python 3.5
 opencv 3.1

・darknetのインストール

GitHub - pjreddie/darknet: Convolutional Neural Networks
ここからdarknetをダウンロード。Makefileを編集。

 GPU=1
 CUDNN=1
 OPENCV=1
 DEBUG=0
 OPENMP=0
 LIBSO=0
 
GPUOPENCVを1に変更。
そしたら、端末で”make”と打って実行。
 
・自分の場合以下のエラーが出ました。 
gcc -DOPENCV `pkg-config --cflags opencv` -DGPU -I/usr/local/cuda/include/ -DCUDNN -Wall -Wfatal-errors -Ofast -DOPENCV -DGPU -DCUDNN -I/usr/local/cudnn/include -c ./src/gemm.c -o obj/gemm.o
In file included from ./src/gemm.c:3:0:
./src/cuda.h:14:26: fatal error: cuda_runtime.h: そのようなファイルやディレクトリはありません
compilation terminated.
Makefile:89: ターゲット 'obj/gemm.o' のレシピで失敗しました
make: *** [obj/gemm.o] エラー 1
 
解決方法は以下のリンクにあるのですが、MakefileのCOMMONとLDFLAGSのCUDAのパスを書き換えることで解消できました。
CUDA8の人は以下のような書き方になります。

 COMMON+= -DGPU -I/usr/local/cuda/include/
 CFLAGS+= -DGPU
 LDFLAGS+= -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand
  ↓ ↓ ↓
 COMMON+= -DGPU -I/usr/local/cuda-8.0/include/
 CFLAGS+= -DGPU
 LDFLAGS+= -L/usr/local/cuda-8.0/lib64 -lcuda -lcudart -lcublas -lcurand

problem compiling · Issue #9 · cvjena/darknet · GitHub

・学習したい画像の用意

 *画像に写っているものが何なのかを判別したいときは以下の画像で学習可能です。
f:id:weekendproject9:20180310163519p:plain
 しかし、部屋や街の中のシーン中に何が写っているかを判別したいときは以下ようなの画像を用意する必要があります。
f:id:weekendproject9:20180310163705p:plain
 
以下のプログラムはwebカメラで対象物の画像を保存するものになります。
また、ネットで集めてもいいです。

-- capture.py --

# -*- coding: utf-8 -*-
import numpy as np
import cv2

# webカメラの準備
cap = cv2.VideoCapture(0)
i=0
while(True):
	# フレームをキャプチャする
	ret, frame = cap.read()

	# 画面に表示する
	cv2.imshow('frame',frame)

	# キーボード入力待ち
	key = cv2.waitKey(1) & 0xFF

	# qが押された場合は終了する
	if key == ord('q'):
		break
	# sが押された場合は保存する
	if key == ord('s'):
		path = str(i) + ".jpg" 
		cv2.imwrite(path,frame)
		i+=1

# キャプチャの後始末と,ウィンドウをすべて消す
cap.release()
cv2.destroyAllWindows()

「操作方法」
 コマンド 「 python capture.py 」 で起動
 キーボード ”s”で1枚保存。画像名の数字は自動で増える
 キーボード ”q”で終了
 

・対象物にクラス名のラベルをつけていく

GitHub - puzzledqs/BBox-Label-Tool at multi-class
BBoxのブランチmulti-classを利用し、画像中の対象物の範囲に枠をつけ、クラス名のラベルをつけていきます。
 
以下手順  
1. main.pyの134行目と152行目の拡張子をJPGからjpgに変更する。
2. BBoxフォルダ内のclass.txtに自分が認識させたいクラス名を書く。以下例:

   car
   human
   pen
   dog
   cat 
3. BBoxのimagesとlabelsの中にフォルダを002の数値で作ります。(別の数字でもいいです。)
4. imagesに作ったフォルダの中に学習させたい画像を移動させます
5. コマンド 「 python2 main.py 」にてBBox起動 (*python2で起動してください)
6. BBox画面上部のimageDirの項目に学習画像を入れたフォルダ名を書いて”Load”をクリック
7. 右上のプルダウンでクラス名を選択して”ComfirmClass”をクリック
8. 領域を左クリックして囲んで、右に出力される結果で間違いなければ下の”Next”をクリック
  (操作を間違ったらDeleteとかClearAllをする)
9.全画像に領域をつけて終了。(NEXTで次の画像にいかないと終了と判断)
10. labelsフォルダ内の自分が作成したフォルダに画像と同じ名前のファイルがあるか確認

・学習データの水増し(必須ではありません)

何千枚、何万枚もの学習データを自分で用意するのは大変なので、プログラムで画像を増やします。
このとき、画像をぼやかしたりやノイズを与えたりしています。
(長いので一番下にプログラム記載(increase_picture.py))
 
1.BBoxのmain,pyと同じところにincrease_picture.pyを置く
2.ImagesとLabelsフォルダ内に"output"というフォルダを作成
3.コマンド 「 python increase_picture.py Images/002 Labels/002 」 (自分のフォルダ名に合わせる)
4.画像が水増しされており、元画像に加工がされているのを確認

 

・学習用データの変換

BBoxは領域とラベルをつけるだけなので、これをdarknetで学習できる形に変換します。
darknet/convert.py at master · Guanghan/darknet · GitHub
ここのプログラムを元に多クラス用に書き換えました。(長いので一番下にプログラム記載(convert.py))
1.BBoxフォルダ一番上の階層に”convert.py”でプログラムを作ります
2.BBoxフォルダ一番上の階層に"convertLabels"というフォルダを作って中に"train"フォルダを作ります
3.コマンド 「 python convert.py 」 で変換します
4.結果をconvertLabels/train内のテキストを見て、下のように一番左にクラス番号、その後に枠の座標値が割合で書かれているのを確認します

 データの並び:【カテゴリ番号 オブジェクトの中心x座標  オブジェクトの中心y座標  オブジェクトの幅  オブジェクトの高さ】
 出力結果例 : 0      0.59609375      0.16041666666666665   0.3484375   0.2791666666666667
 

・darknetで学習

1.darknetフォルダのdata内に”obj”フォルダを作成
2.How to train YOLOv2 to detect custom objects
  からprocess.pyを持ってきてobjフォルダに置く
3.objフォルダにBBOXフォルダのimages/outputの画像たちをobjフォルダにコピー
4.コマンド 「 python process.py 」 を起動させて、test.txt(評価用)とtrain.txt(学習用)ができているのを確認
5.BBoxフォルダ内のconvertLabels/train内のテキスト達をdarknet/data/obj内にコピー
6.darknet/data内に"data.names"を作成後、自分の学習させるクラス名を記入(BBoxフォルダのclass.txtの中身と一緒)

   car
   human
   pen
   dog
   cat 
7.darknet/cfgに”obj.data”を作成。中身は以下のようにする
 classes= 5
 train = data/obj/train.txt
 valid = data/obj/test.txt
 names = data/obj.names
 backup = backup/
ここでclasses= 5 は学習させるクラスの数なので自分の数値に合わせる。今回の例だと5になる。
8.darknetフォルダ内に学習結果を保存するための”backup”フォルダを作成
9.darknet/cfg/yolo-obj.cfgの内容を編集しますが、以下参考HPの内容をそのまま記述させていただきます。

cfg/yolo-obj.cfgはもとからcfgディレクトリの中にある.cfgファイルをコピーしてちょっと編集してyolo-obj.cfgとします。今回はyolo-voc.cfgファイルをコピーして使いますが、他のモデル(tiny-yolo.cfg等)が使いたかったら適宜変えてください。
コピーしてリネームしたyolo-obj.cfgを少し編集します。
3行目:batch=64 にします。学習ステップごとに使い画像の枚数です。
4行目:subdivisions=8 にします。バッチが8で除算されます。強力なGPUを積んでればこれを減らすか、バッチを増やすことができます。
244行目:classes=1 にします。クラス数です。
237行目:filters=30 にします。フィルターの大きさです。filters=(classes + 5) * 5 と決まっています?
YOLOv2を使って自前のデータを学習させて認識させるまで。 - 可変ブログ

参考HP内容から変更する部分は、”244行目:classes=1” の部分です。ここを自分のクラスの数値にして下さい。(項目の7と同じ数)
また、自分の場合はgtx1070なのでこの設定で学習できますが、もしgtx960、gtx950などのメモリ数が少ない人はメモリが足らないこともあるかと思います。
そうした場合は学習結果が悪くなりますが、以下を変更することでなんとか学習をすることができます。
3行目 batch を32とかに減らす
258行目 random=0 これは画像をいじりながら学習する設定ですが、0にすることで無しにできます。
もっとありますが試したのはこの2つです。
 
10. https://pjreddie.com/media/files/darknet19_448.conv.23 から初期データをダウンロードして,darknetフォルダ内に移動。
11.以下のコマンドで学習開始

 ./darknet detector train cfg/obj.data cfg/yolo-obj.cfg darknet19_448.conv.23
 
 
12.学習ができているか確認
画像を読み込ませて確認
 ./darknet detector test cfg/obj.data cfg/yolo-obj.cfg backup/yolo-obj_***.backup data/testimage.jpg 
                               ↑学習した数値入れる   ↑画像の場所は自分の環境にあわせる

webカメラで確認する場合
 ./darknet detector demo cfg/obj.data cfg/yolo-obj.cfg backup/yolo-obj_***.weights
                                ↑学習した数値入れる
 

・プログラム

・画像の水増し
yoloの画像を生成するまで · GitHub

-- increase_picture.py --

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

import cv2
import numpy as np
import sys
import glob
import os
import re
import numpy as np
from os import walk, getcwd

# ヒストグラム均一化
def equalizeHistRGB(src):

    RGB = cv2.split(src)
    Blue   = RGB[0]
    Green = RGB[1]
    Red    = RGB[2]
    for i in range(3):
        cv2.equalizeHist(RGB[i])

    img_hist = cv2.merge([RGB[0],RGB[1], RGB[2]])
    return img_hist

# ガウシアンノイズ
def addGaussianNoise(src):
    row,col,ch= src.shape
    mean = 0
    var = 0.1
    sigma = 15
    gauss = np.random.normal(mean,sigma,(row,col,ch))
    gauss = gauss.reshape(row,col,ch)
    noisy = src + gauss

    return noisy

# salt&pepperノイズ
def addSaltPepperNoise(src):
    row,col,ch = src.shape
    s_vs_p = 0.5
    amount = 0.004
    out = src.copy()
    # Salt mode
    num_salt = np.ceil(amount * src.size * s_vs_p)
    coords = [np.random.randint(0, i-1 , int(num_salt))
              for i in src.shape]
    out[coords[:-1]] = (255,255,255)

    # Pepper mode
    num_pepper = np.ceil(amount* src.size * (1. - s_vs_p))
    coords = [np.random.randint(0, i-1 , int(num_pepper))
              for i in src.shape]
    out[coords[:-1]] = (0,0,0)
    return out

if __name__ == '__main__':
    # ルックアップテーブルの生成
    min_table = 50
    max_table = 205
    diff_table = max_table - min_table
    gamma1 = 0.75
    gamma2 = 1.5

    LUT_HC = np.arange(256, dtype = 'uint8' )
    LUT_LC = np.arange(256, dtype = 'uint8' )
    LUT_G1 = np.arange(256, dtype = 'uint8' )
    LUT_G2 = np.arange(256, dtype = 'uint8' )

    LUTs = []

    # 平滑化用
    average_square = (10,10)

    # ハイコントラストLUT作成
    for i in range(0, min_table):
        LUT_HC[i] = 0

    for i in range(min_table, max_table):
        LUT_HC[i] = 255 * (i - min_table) / diff_table

    for i in range(max_table, 255):
        LUT_HC[i] = 255

    # その他LUT作成
    for i in range(256):
        LUT_LC[i] = min_table + i * (diff_table) / 255
        LUT_G1[i] = 255 * pow(float(i) / 255, 1.0 / gamma1)
        LUT_G2[i] = 255 * pow(float(i) / 255, 1.0 / gamma2)

    LUTs.append(LUT_HC)
    LUTs.append(LUT_LC)
    LUTs.append(LUT_G1)
    LUTs.append(LUT_G2)

    wd = getcwd()
    # 画像の読み込み
    iamge_path = sys.argv[1]
    files = glob.glob(iamge_path + '/*.jpg')
    print(files)

    for item_index, file in enumerate(files):
        img_src = cv2.imread(file, 1)

        trans_img = []
        trans_img.append(img_src)

        # LUT変換
        for i, LUT in enumerate(LUTs):
            trans_img.append( cv2.LUT(img_src, LUT))

        # 平滑化
        trans_img.append(cv2.blur(img_src, average_square))

        # ヒストグラム均一化
        trans_img.append(equalizeHistRGB(img_src))

        # ノイズ付加
        trans_img.append(addGaussianNoise(img_src))
        trans_img.append(addSaltPepperNoise(img_src))

        # 反転
        flip_img = []
        for img in trans_img:
            flip_img.append(cv2.flip(img, 1))
        trans_img.extend(flip_img)

        # 保存
        #if not os.path.exists("training_images"):
        #    os.mkdir("training_images")

        base = os.path.splitext(os.path.basename(file))[0] + "_"
        item_txt = os.path.splitext(os.path.basename(file))[0] + '.txt'
        img_src.astype(np.float64)
        for i, img in enumerate(trans_img):
            # 比較用
            # cv2.imwrite("images/003/" + base + str(i) + ".jpg" ,cv2.hconcat([img_src.astype(np.float64), img.astype(np.float64)]))
            #9以下は反転していない
            coordinate = ''
            labels_path = sys.argv[2]

            file = open(labels_path + '/' + item_txt, 'r')
            if i <= 8:
                coordinate = file.read()
            #9以上は反転している
            else:
                line = file.readline()
                coordinate = line

                while line:
                    line = file.readline()
                    words = re.split(" +", line)
                    if len(words) >= 2:
                        x = words[0]
                        y = words[1]
                        width = words[2]
                        height = words[3]
                        class_name = words[4]
                        img_width = img.shape[1]

                        x = int(img_width) - int(width)
                        width = int(img_width) - int(words[0])

                        coordinate = coordinate + ' '.join([
                            str(x).strip(),
                            str(y).strip(),
                            str(width).strip(),
                            str(height).strip(),
                            class_name
                        ]) + '\n'

            file.close()

            f = open("Labels/output/" + base + str(i) + ".txt", 'w')
            f.write(coordinate)
            f.close()

            cv2.imwrite("Images/output/" + base + str(i) + ".jpg" ,img)

 
・学習データ用に変換

-- convert.py --

# -*- coding: utf-8 -*-
"""
Created on Wed Dec  9 14:55:43 2015
This script is to convert the txt annotation files to appropriate format needed by YOLO 
@author: Guanghan Ning
Email: gnxr9@mail.missouri.edu
"""

import os
from os import walk, getcwd
from PIL import Image

classes = ["train"]

def convert(size, box):
    dw = 1./size[0]
    dh = 1./size[1]
    x = (box[0] + box[1])/2.0
    y = (box[2] + box[3])/2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)
    
    
"""-------------------------------------------------------------------""" 

""" Configure Paths"""   
mypath = "Labels/output/"
outpath = "convertLabels/train/"
imgpath = "/Images/output"

cls = "train"
if cls not in classes:
    exit(0)
cls_id = classes.index(cls)

wd = getcwd()
""" GET class text file list """
class_path = "class"
class_file = open('%s/%s.txt'%(wd, class_path), 'r')
class_list = class_file.read().split('\n')   #for ubuntu, use "\r\n" instead of "\n"
print(class_list)

list_file = open('%s/%s_list.txt'%(wd, cls), 'w')

""" Get input text file list """
txt_name_list = []
for (dirpath, dirnames, filenames) in walk(mypath):
    txt_name_list.extend(filenames)
    break
#print(txt_name_list)

""" Process """
for txt_name in txt_name_list:
    # txt_file =  open("Labels/stop_sign/001.txt", "r")
    
    """ Open input text files """
    txt_path = mypath + txt_name
    #print("Input:" + txt_path)
    txt_file = open(txt_path, "r")
    lines = txt_file.read().split('\n')   #for ubuntu, use "\r\n" instead of "\n"
    
    """ Open output text files """
    txt_outpath = outpath + txt_name
    #print("Output:" + txt_outpath)
    txt_outfile = open(txt_outpath, "w")
    
    
    """ Convert the data to YOLO format """
    ct = 0
    for line in lines:
        #print('lenth of line is: ')
        #print(len(line))
        #print('\n')
        if(len(line) >= 2):
            ct = ct + 1
            print(line)
            elems = line.split(' ')
            #print(elems)
            xmin = elems[0]
            xmax = elems[2]
            ymin = elems[1]
            ymax = elems[3]
            class_name = elems[4]
            #print(class_name)
            #
            img_path = str('%s/%s.jpg'%(wd+imgpath,os.path.splitext(txt_name)[0]))
            print("img_path:",img_path)
            #t = magic.from_file(img_path)
            #wh= re.search('(\d+) x (\d+)', t).groups()
            im=Image.open(img_path)
            w= int(im.size[0])
            h= int(im.size[1])
            #w = int(xmax) - int(xmin)
            #h = int(ymax) - int(ymin)
            # print(xmin)
            #print(w, h)
            b = (float(xmin), float(xmax), float(ymin), float(ymax))
            bb = convert((w,h), b)
            #print(bb)
            print('\n')
            i=0
            for class_num in class_list:
               if class_name == class_num:
                 class_id = i
               i+=1
            txt_outfile.write(str(class_id) + " " + " ".join([str(a) for a in bb]) + '\n')

    """ Save those images with bb into list"""
    if(ct != 0):
        list_file.write('%s/images/%s/%s.jpg\n'%(wd, cls, os.path.splitext(txt_name)[0]))
                
list_file.close()