目的
YOLOやSSDで自作のデータセットを用いて物体認識を行う際、大量な画像とラベリング処理が必要となる。
今回は、ラベリング処理にかかる時間を短縮するためのプログラムを作成した。
※クラスを新たに追加して検出する際は、100枚程度を手作業でラベリングすることと、voc_annotation.pyの変更が必要。
使用したもの
物体認識アルゴリズム
[YOLO]https://github.com/qqwweee/keras-yolo3
[SSD]https://github.com/rykov8/ssd_keras
ラベリング用ソフト
処理の流れ
- 学習(python train.py)
- yolo_labeling.py の model_path を1で作成したものに変更
- 検出(python yolo_img.py)
- "Input filepath" のあとに検出画像があるディレクトリを入力
- 検出結果からAnnotationファイル作成
なお、デフォルトのクラス内に存在するものであれば1.2は省略可。
プログラムの詳細は最後に示す。
結果
未学習画像に対して、自動的にラベリングができる。
これを使用することで学習データを増やす際のラベリング処理にかかる時間を短縮することができる。
結論
デフォルトのクラスにない新たなものを認識する場合は大量のデータセットが必要となる。実際YOLOでは、単純なものであれば100枚程度で認識することはできたが、似たものが存在する場合は認識できなかった。また、バウンディングボックスの位置にもずれがあった。
このプログラムを使用することで、100枚程度は手作業でラベリングする必要があるがその後の何百枚を短時間でラベリングすることができる。
今回はラベリングソフトに合わせた形式で出力を行った。そのため、このプログラム実行後にvoc_annotation.pyを実行して、train.txtを作成する必要がある。今後はこの処理も短縮し、train.txtに自動的に追加する機能を持たせたい。
また、新しいものを物体認識するためには、voc_annotation.py の変更などを行う必要がある。今後はこれについて説明する記事も書きたい。
プログラム
プログラムはgithub上に公開済 https://github.com/ptxyasu/keras-yolo3
ここでは、keras-yolo3に追加したプログラムの説明を行う。
デフォルトのyolo.pyをラベリング作成用に修正したものがyolo_labeling.pyである。
ラベリング処理用に make_xml, make_xml_object を追加した。
make_xmlは主なタグの作成とファイル名やファイルパス、画像サイズをセットする。
make_xml_objectは検出されたオブジェクトごとにクラス名と位置をセットする。
※Annotationファイルの形式は参考にあるラベリングソフトに合わせてあることに注意
def make_xml(self,image,img_name,img_path):
#parent
annotation = ET.Element('annotation')
#child
folder = ET.SubElement(annotation, 'folder')
filename = ET.SubElement(annotation, 'filename')
path = ET.SubElement(annotation, 'path')
source = ET.SubElement(annotation, 'source')
size = ET.SubElement(annotation, 'size')
segmented = ET.SubElement(annotation, 'segmented')
database = ET.SubElement(source, 'database')
width = ET.SubElement(size,'width')
height = ET.SubElement(size,'height')
depth = ET.SubElement(size,'depth')
#set
folder.text = "JPEGImages"
filename.text = img_name
path.text = os.getcwd() +"/"+ img_path
database.text = "Unknown"
imgw,imgh = image.size
width.text = str(imgw)
height.text = str(imgh)
depth.text = "3"
segmented.text = "0"
return annotation
def make_xml_object(self,annotation,class_name,x1,y1,x2,y2):
object = ET.SubElement(annotation, 'object')
name = ET.SubElement(object,'name')
pose = ET.SubElement(object,'pose')
truncated = ET.SubElement(object,'truncated')
difficult = ET.SubElement(object,'difficult')
bndbox = ET.SubElement(object,'bndbox')
xmin = ET.SubElement(bndbox,'xmin')
ymin = ET.SubElement(bndbox,'ymin')
xmax = ET.SubElement(bndbox,'xmax')
ymax = ET.SubElement(bndbox,'ymax')
name.text = class_name
pose.text = "Unspecified"
truncated.text = "0"
difficult.text = "0"
xmin.text = str(x1)
ymin.text = str(y1)
xmax.text = str(x2)
ymax.text = str(y2)
return annotation
また、上の二つのをプログラムに実装するためにdetect_image の変更を行った。
make_xmlは写真1枚につき1回、make_xml_objectは検出されたオブジェクトの数だけ実行される。
def detect_image(self,image,img_name,img_path,annotation_path):
start = timer()
if self.model_image_size != (None, None):
assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required'
assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required'
boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
else:
new_image_size = (image.width - (image.width % 32),
image.height - (image.height % 32))
boxed_image = letterbox_image(image, new_image_size)
image_data = np.array(boxed_image, dtype='float32')
print(image_data.shape)
image_data /= 255.
image_data = np.expand_dims(image_data, 0) # Add batch dimension.
out_boxes, out_scores, out_classes = self.sess.run(
[self.boxes, self.scores, self.classes],
feed_dict={
self.yolo_model.input: image_data,
self.input_image_shape: [image.size[1], image.size[0]],
K.learning_phase(): 0
})
print('Found {} boxes for {}'.format(len(out_boxes), 'img'))
font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
size=np.floor(8e-3 * image.size[1] + 0.5).astype('int32'))
thickness = (image.size[0] + image.size[1]) // 500
ann = self.make_xml(image,img_name,img_path)
for i, c in reversed(list(enumerate(out_classes))):
predicted_class = self.class_names[c]
box = out_boxes[i]
score = out_scores[i]
label = '{} {:.2f}'.format(predicted_class, score)
draw = ImageDraw.Draw(image)
label_size = draw.textsize(label, font)
top, left, bottom, right = box
top = max(0, np.floor(top + 0.5).astype('int32'))
left = max(0, np.floor(left + 0.5).astype('int32'))
bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32'))
right = min(image.size[0], np.floor(right + 0.5).astype('int32'))
print(label, (left, top), (right, bottom))
ann = self.make_xml_object(ann,predicted_class,left,bottom,right,top)
if top - label_size[1] >= 0:
text_origin = np.array([left, top - label_size[1]])
else:
text_origin = np.array([left, top + 1])
# My kingdom for a good redistributable image drawing library.
for i in range(thickness):
draw.rectangle(
[left + i, top + i, right - i, bottom - i],
outline=self.colors[c])
draw.rectangle(
[tuple(text_origin), tuple(text_origin + label_size)],
fill=self.colors[c])
draw.text(text_origin, label, fill=(0, 0, 0), font=font)
del draw
end = timer()
print(end - start)
tree = ET.ElementTree(ann)
annotation_name,ext = img_name.split(".")
annotation_name = "/" + annotation_name + ".xml"
annotation = annotation_path + annotation_name
tree.write(annotation,"utf-8",True)
return image
また、デフォルトではyolo_video.py にオプションimageをつけて実行することで、画像からの物体認識が行えるが、画像名を毎回手入力しなければならない。複数画像を一気に行うためにyolo_video.pyを変更したものがyolo_img.pyである。
”Input filepath”と表示されたら、対象画像があるディレクトリを入力する。そのディレクトリ内にある画像全てに物体認識が行われ、検出結果を表す画像とAnnotationファイルが出力される。
import sys
import argparse
import os
#from yolo import YOLO, detect_video
from yolo_labeling import YOLO, detect_video
from PIL import Image
def detect_img(yolo):
while True:
file_path = input('Input filepath:')
img_list = os.listdir(file_path)
if file_path[-1] != "/":
file_path += "/"
if os.path.exists(file_path+"result") != True:
os.mkdir(file_path+"result")
if os.path.exists(file_path+"Annotations") != True:
annotation_path = file_path+"Annotations"
os.mkdir(annotation_path)
for i in range(len(img_list)):
img_name = img_list[i]
print(img_name)
img_path = file_path + img_name
try:
image = Image.open(img_path)
except:
print('Open Error! Try again!')
continue
else:
#r_image = yolo.detect_image(image)
#r_image.show()
r_image = yolo.detect_image(image,img_name,img_path,annotation_path)
r_image.save(file_path+"result/"+img_name)
yolo.close_session()
FLAGS = None
if __name__ == '__main__':
# class YOLO defines the default value, so suppress any default here
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
'''
Command line options
'''
parser.add_argument(
'--model', type=str,
help='path to model weight file, default ' + YOLO.get_defaults("model_path")
)
parser.add_argument(
'--anchors', type=str,
help='path to anchor definitions, default ' + YOLO.get_defaults("anchors_path")
)
parser.add_argument(
'--classes', type=str,
help='path to class definitions, default ' + YOLO.get_defaults("classes_path")
)
parser.add_argument(
'--gpu_num', type=int,
help='Number of GPU to use, default ' + str(YOLO.get_defaults("gpu_num"))
)
FLAGS = parser.parse_args()
print("Image detection mode")
detect_img(YOLO(**vars(FLAGS)))