[Python]YOLOv3を自学習させたデータで試してみる

[Python]YOLOv3を自学習させたデータで試してみる

どうも、久しぶりの投稿&Python記事となります。

今回は本格的な機械学習と使ったプログラムとなります。

ですが、先に言わせていただきますとこの記事を使っての環境構築はお勧めしません。

なぜなら、Ubuntu環境とwindows10環境を行ったり来たりしてるからです。

どっちもかなり悪戦苦闘しながらやっていたので

エラー出てはあっち行って、エラー出てはこっちいって状態でした。

簡単にYOLOv3を試したいならUbuntuがおすすめです。

しかし、Ubuntuは導入が簡単な分、学習時間がとても長いです。(CPUのみで学習してるため)

私はVsitualStudioで動かしているのでGPUでは動かしていません。※導入の仕方は知らない

・私が行った流れ

最初にUbuntuでdarknetを入れ、BBoxで学習データ用の画像を作成し、darknetで学習させました。

その後、Ubuntuの学習ではメモリ不足で強制終了(対策は可能)が続いたのでWindow10での環境を構築し、モデルの学習をしました。

最初から新しく始めるのであれば、環境をどちらかに絞るべきでしたね。自分への戒めを込めていってます。

・Ubuntuでの環境構築

まずは、darknetを落とすところから

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

ダウンロードが完了出来たら次は学習したい画像の用意をします。

画像はスクレイピングなり、どこかから一括でダウンロードするなりして集めてください。

画像を学習させるためにBBoxを使います。

BBoxにはマルチクラス用とそうでない2種類があります。

覚えさせたいものが1種類の場合はこちら

git clone https://github.com/puzzledqs/BBox-Label-Tool.git

マルチクラス用はこちらとなります。

git clone https://github.com/puzzledqs/BBox-Label-Tool.git

マルチクラス用はダウンロードできたらBBoxフォルダ内のclass.txtを開いてクラス名を書き換えます。

例:

cat

dog

bird

BBoxを起動させる前にフォルダ内のmain.pyを開いて少し編集します。

from Tkinter import *

import tkMessageBox

上のコードを

try:

import tkinter

import tkinter.messagebox

except:

import Tkinter as tkinter

import tkMessageBox

from tkinter import *

<p “=””>に変更。

import ttk

from tkinter import ttk

に変更?(これは確かしなくてよかったような、ttkのimportでエラーが出たらここを編集してください)

検索・置換で[JPG]となっている箇所を[jpg]に書き換え

私の場合は保存した画像はすべてjpgでしたのこのようにしましたが、拡張子が違う方はその都度書き換えてください。

多分、拡張子は統一していないとダメだと思います。

それではBBoxを動かしていきましょう。

BBoxはPython2用のプログラムなのでBBoxのフォルダで以下を実行しましょう。

python2 main.py

私はPython3で動かしたかったので、main.pyの中身のprintやらを編集したりしました。

上記のコードでエラーが出た方は多分モジュールをインストールしていないとかだと思うので、 エラーが表示されたモジュールをインストールしてください。

無事に実行出来たら、こんな画面が出てくると思います。

こちらの画面はBBoxのマルチクラス用となります。
右上のほうにdogと書いてある項目がありますがこれがクラスの名前となります。
普通のBBoxではこの項目は出てこないです。
マルチ用はこの項目でクラス分けをしていきます。

これで順次画像を枠で囲ってラベルを作成していきますが、その前に画像をBBoxフォルダに入れます。
~\BBox-Label-Tool-multi-class\Images\001
このファルダの中にデフォルトで入っている画像がありますので削除して、学習させたい画像を入れます。

そうして、もう一度BBoxのmain.pyを起動させて
Image Dir:の項目に[001]と記入し、Loadを押すと001フォルダ内の画像が表示されます。(順番はランダム)

そして覚えさせたい箇所をマウスカーソルでポイントすると枠が作成できます。
下の方にある[Next]ボタンを押すと画像が勝手に保存され、自動でラベルも作成してくれます。

※注意点
・画像のファイル名は余分な記号は取り除いておく(筆者がURLの名前そのままにしておいたために、.[ピリオド]をご認識してずっと同じ名前で保存されていた)
・10個以上の枠(ラベル)はエラーが起きるので、これも削除するかtxtファイル内の最初の一文字目を9に減らして、下の座標もその分だけ減らしてください。

枠を間違えたときは右下の項目の[Delete],[ClearAll]で削除できます。
これですべての画像を順次やっていきましょう。画像ファイルが多いとかなり根気がいると思いますが、頑張ってください。

次のステップに行く前に、また更に注意点があります。
※注意点
・枠(ラベル)を作成しなかった画像があれば、削除しておく必要があります。この後に出てくる何らかのプログラムでエラーが出たはずです。
Images\001下の画像ファイルとLabels\001下のtxtファイルを両方削除しておきましょう。
txtファイルの名前は画像ファイルと一緒なので手動で検索して削除するか、中身が0となっているので簡単なプログラムでも削除できます。

では次のステップの画像の水増しを行います。

必要のない方はとばしてもらって構いません。
画像を何万枚も用意するのは大変なので、画像をいろいろ加工して学習データを増やします。
参考記事:http://weekendproject9.hatenablog.com/entry/2018/03/10/165721
上記の記事を参考にさせていただき、以下のプログラムを走らせます。
BBoxのmain.pyと同じフォルダ内に[increase_picture.py]として保存。

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)

保存出来たら、プログラムを実行する前に出力用のフォルダを作成します。
BBoxのフォルダ下のImagesとLabelsがありますが、その2つの下に[output]という名前でフォルダを作ってください。
作成出来たらBBoxフォルダで

python increase_picture.py Images/001 Labels/001

とプログラムを実行させてください。
成功すれば、outputフォルダに水増しした画像が入っているはずです。

続いて、作成したラベルのデータを学習可能な形にします。
BBoxで作成したラベルはdarknetとは違う出力の仕方なので、darknet用に変換します。
以下のプログラムを[convert.py]としてBBoxフォルダ下に保存してください。
darknet/convert.py at master · Guanghan/darknet · GitHub

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()

これもまた、プログラムを実行する前に出力用フォルダを作成します。
BBoxフォルダ下に[convertLabels]というフォルダを作り、その下に[train]というフォルダを作成します。
これで出力用フォルダは作成できたので、プログラムを実行します。

python convert.py

実行が問題なくできたら、train下にファイルができているか確認しましょう。

0 0.49722222222222223 0.4922222222222222 0.81 0.6866666666666666

txtファイル内がこんな感じになっていたらOKです。

いよいよdarknetで学習させていきますが、
その前に準備から
まず、darknet\data下に[obj]フォルダを作成してください。
次に、How to train YOLOv2 to detect custom objectsから[process.py]を持ってきてobjフォルダに入れてください。
念のために、プログラムを表示しておきます。 以下がprocess.pyのプログラムです。

import glob, os

# Current directory
current_dir = os.path.dirname(os.path.abspath(__file__))

# Directory where the data will reside, relative to ‘darknet.exe’
path_data = ‘data/obj/’

# Percentage of images to be used for the test set
percentage_test = 10;

# Create and/or truncate train.txt and test.txt
file_train = open(‘train.txt’, ‘w’)
file_test = open(‘test.txt’, ‘w’)

# Populate train.txt and test.txt
counter = 1
index_test = round(100 / percentage_test)
for pathAndFilename in glob.iglob(os.path.join(current_dir, “*.jpg”)):
title, ext = os.path.splitext(os.path.basename(pathAndFilename))

if counter == index_test:
counter = 1
file_test.write(path_data + title + ‘.jpg’ + “\n”)
else:
file_train.write(path_data + title + ‘.jpg’ + “\n”)
counter = counter + 1

これは学習用と評価用に分けるプログラムです。

そしたら、objフォルダの中に先ほど生成したoutputフォルダ内の画像を入れます。(水増ししていない人はImages\001の中身)
[python prcess.py]を実行させて、test.txtとtrain.txtが生成されているのを確認してください。

次はBBox\data内の変換したconvertLabels\train内のラベルテキストをdarknet\data\objにコピーしてください。
画像をコピー出来たら、darknet\data下に[data.names]というファイルを作成しましょう。
ファイル内には先ほどのBBox内のclass.txtと同じように学習させたいクラス名を記入してください。
例:data.names

dog
cat
horse

長いですが、あと少しです。
次はdarknet\cfg下に[obj.data]を作成。
obj.dataの中身は以下のようにしてください。

classes= 3
train = data/obj/train.txt
valid = data/obj/test.txt
names = data/obj.names

backup = backup/

classes=3の数値は自分の学習させたい数値です、class.txtやdata.namesに書いた数を一緒にしてください。

学習結果を出力するためにdarknetフォルダ下に[backup]というフォルダを作成。
darknet\cfg\yolo3-voc.cfgというファイルがあるので、コピーを作成して名前を[yolo3-obj.cfg]としてください。
そしてyolo3-obj.cfgの中身を書き換えます。

3行目 batch=1 を batch=64 書き換え(これはちょっとよくわからない)
4行目 subdivisions=1 を subdivisions=32 に変更(私はメモリに不安があったので32にしてます、CPUが高性能な方は8,16でも大丈夫なはずです。)
ファイル内の[yolo]という項目が三か所あるので検索して、
・一つ上の項目の[convolutional]の中のfiltersをfilters=(classes + 5) * 3に変更。(クラスが3つの場合は24)
・[yolo]項目内のclassesを学習させたいクラス数に変更。
上二つは三か所とも変更してください。

そして、最後にdarknetのサイトからモデルをダウンロードします。 https://pjreddie.com/media/files/darknet53.conv.74
ダウンロードしたファイルをdarknetフォルダ下に保存しておきます。

以上で、下準備が終わりました。かなり長かったですね。
わかりにくい部分が多いと思うので、もう少しわかりやすく書けたらいいなと思います。
YOLOv3やdarknetの仕組み自体筆者も知らないので、動かして楽しむぐらいにしておきましょう。

いざ学習開始

端末でdarknetフォルダにいき、以下のコマンドを打ちます。

すると、ガリガリパソコンが学習を始めてくれるはずです。
強制終了と表示され、処理が止まってしまう場合にはyolo3-obj.cfgの中身のsubdivisionsの数値を大きくしましょう。
また、何らかの画像で処理が止まってしまう場合には、その画像を削除して次に進めます。

そして学習結果の確認は以下のコマンドを実行します。

./darknet detector test cfg/obj.data cfg/yolo-obj.cfg backup/yolo3-obj_***.backup data/test.jpg

***にはbackupフォルダに生成されている重みファイル名を入力。
最後のtest.jpgは確認したい画像ファイル名。

Ubuntu環境でのYOLO構築はこんな感じだと思います。
Windowsとごっちゃになってるかも知れないので、間違えているところがあればご指摘ください。

・Windowでの環境構築

続いてwindowsでの環境構築のやり方を書いていきます。
まず、準備するものがいくつかあります。

CUDA 9.2
cuDNN 7.1
OpenCV3.4.0

CUDAは9.1を推奨されていたが、9.2にでも問題なく使え、
cuDNNも7.0が推奨だが、これも問題なく使えました。
OpenCVは最新versionは3.4.2ですが、Gitのほうに注意書きがあるので3.4.0を使いたいと思います。
OpenCVのダウンロードはこちらから
3.4.0のWindows版をダウンロードして、解凍してください。
(私の場合は、C:\opencv340というフォルダを作り、この中に展開)
そして、Pathを通すため環境変数のPathに

C:\opencv340\opencv\build\x64\vc14\bin

を追加してください。(最初のC:\opencv340は自分の環境に合わせる)

CUDAとcuDNNのインストール方法は割愛させていただきます。
そんなハマるようなとこはなかったはずです。
ざっくりいいますと、CUDAのここのページからCUDA Toolkit 9.2をダウンロード。
cuDNNはこちらのページからDownload cuDNN v7.1.4 (May 16, 2018), for CUDA 9.2をダウンロード。(アカウント作成が必要)
cuDNNをダウンロードできたら、確か3つのファイルがあったと思うので、それをCUDAフォルダにコピーしてください。(詳しくは調べればすぐ出てくるはず)

次に下のリンクからプログラムをダウンロードし、展開してください。
https://github.com/AlexeyAB/darknet
展開したフォルダにbuild/darknet/darknet.slnがあるので、このファイルをVisual Studioで開く。(私はversion2017で開きましたが、2015でも問題ないはず)

・構成の変更
構成をReleaseに変更、プラットフォームをx64に変更

・ビルドツールの変更
プロジェクト名を右クリックでビルドの依存関係>ビルドのカスタマイズに行き、

・ディレクトリの変更
上のタブの[プロジェクト]から[プロパティ]を選択。
[VC++ディレクトリ]のインクルードディレクトリ、ライブラリディレクトリを以下のように追加する。
インクルードディレクトリ:C:\opencv340\opencv\build\include
ライブラリディレクトリ:C:\opencv340\build\x63\vc12\lib

続いて、[C/C++]>[全般]>[追加のインクルードディレクトリ]と[リンカー]>[全般]>[追加のライブラリディレクトリ]にも同じように追加する。

・コンパイル
準備は整ったのでコンパイルを実行しましょう。
[ビルド]>[ソリューションのビルド]などでビルドを実行。
無事にできた方はbuild\darknet\x64の下にdarknet.exeが生成されています。
もし、うまくいかなかった方はエラーを読んで原因を突き止めましょう。(私の場合はCUDAのPathがうまく通っておらず、エラーになっていた)

・動作確認

以下のリンクより重みファイルをダウンロード

https://pjreddie.com/media/files/yolov3.weights

ダウンロードしたファイルをbuild\darknet\x64の下に入れます。
build\darknet\x64のコマンドプロンプトを開き、以下のコマンドを入力

実行結果はこんな感じになります。

うまく動いたでしょうか?
エラーなどが発生した方はコメントなどで教えてください。わかる範囲でお答えできればと思います。

なお、Window環境の場合の独自データ学習もUbuntu環境と同じ手順を踏めば問題なくできます。
フォルダ名などに気を付けて、またWindowでの実行はdarknet.exeと実行形式ファイルとなりますのでそれも気を付けてください。

Pythonカテゴリの最新記事