一、任務
現在用caffe做目標檢測一般需要lmdb格式的數據,而目標檢測的數據和目標分類的lmdb格式的制作難度不同。就目標檢測來說,例如准備SSD需要的數據,一般需要以下幾步:
1.准備圖片並標注groundtruth
2.將圖像和txt格式的gt轉為VOC格式數據
3.將VOC格式數據轉為lmdb格式數據
本文的重點在第2、3步,第一步標注任務用小代碼實現即可。網絡上大家制作數據格式一般是仿VOC0712的,建立各種目錄,很麻煩還容易出錯,現我整理了一下代碼,只要兩個代碼,就可以從圖片+txt格式gt的數據轉化為lmdb格式,不需要額外的文件夾,換其他數據庫也改動非常少,特別方便。
二、准備工作
本文基於已經標注好的數據,以ICDAR2013庫為例,起始數據格式如下:
圖片目錄:ICDAR2013\img\test\*.jpg和ICDAR2013\img\train\*.jpg
gt目錄:ICDAR2013\img\test\gt_*.txt和ICDAR2013\img\train\gt_*.txt
gt的格式為:
三、轉VOC格式
1.建立如下目錄:Annotations、ImageSets、JPEGImages、label
其中Annotations里面建空文件夾test和train,用來存放轉換好的gt的xml形式。當然,可以只建單個,比如只要制作train的數據那就只要建立train文件夾就好了。
ImageSets里面建空文件夾Main,里面存放train.txt和test.txt,txt內容是圖片的名字,不帶.jpg的名字,初始是空的,是通過代碼生成的。
JPEGImages里面建文件夾train和test,並把訓練和測試集圖片對應扔進去。
里面建文件夾train和test,並把訓練和測試集的groundtruth的txt文件對應扔進去。
現在格式如下:
2.使用下面的create_voc_data.py生成xml文件和后續需要的txt文件

import os import numpy as np import sys import cv2 from itertools import islice from xml.dom.minidom import Document def create_list(dataName,img_list_txt,img_path,img_name_list_txt,type): f=open(img_name_list_txt,'w') fAll=open(img_list_txt,'w') for name in os.listdir(img_path): f.write(name[0:-4]+'\n') fAll.write(dataName+'/'+'JPEGImages'+'/'+type+'/'+name[0:-4]+'.jpg'+' ') fAll.write(dataName+'/'+'Annotations'+'/'+type+'/'+name[0:-4]+'.xml'+'\n') f.close() def insertObject(doc, datas): obj = doc.createElement('object') name = doc.createElement('name') name.appendChild(doc.createTextNode('text')) obj.appendChild(name) bndbox = doc.createElement('bndbox') xmin = doc.createElement('xmin') xmin.appendChild(doc.createTextNode(str(datas[0]).strip(' '))) bndbox.appendChild(xmin) ymin = doc.createElement('ymin') ymin.appendChild(doc.createTextNode(str(datas[1]).strip(' '))) bndbox.appendChild(ymin) xmax = doc.createElement('xmax') xmax.appendChild(doc.createTextNode(str(datas[2]).strip(' '))) bndbox.appendChild(xmax) ymax = doc.createElement('ymax') ymax.appendChild(doc.createTextNode(str(datas[3]).strip(' '))) bndbox.appendChild(ymax) obj.appendChild(bndbox) return obj def txt_to_xml(labels_path,img_path,img_name_list_txt,xmlpath_path,bb_split,name_size): img_name_list=np.loadtxt(img_name_list_txt,dtype=str) name_size_file=open(name_size,'w') for img_name in img_name_list: print(img_name) imageFile = img_path + img_name + '.jpg' img = cv2.imread(imageFile) imgSize = img.shape name_size_file.write(img_name+' '+str(imgSize[0])+' '+str(imgSize[1])+'\n') sub_label=labels_path+'gt_'+img_name+'.txt' fidin = open(sub_label, 'r') flag=0 for data in islice(fidin, 1, None): flag=flag+1 data = data.strip('\n') datas = data.split(bb_split) if 5 != len(datas): print img_name+':bounding box information error' exit(-1) if 1 == flag: xml_name = xmlpath_path+img_name+'.xml' f = open(xml_name, "w") doc = Document() annotation = doc.createElement('annotation') doc.appendChild(annotation) folder = doc.createElement('folder') folder.appendChild(doc.createTextNode(dataName)) annotation.appendChild(folder) filename = doc.createElement('filename') filename.appendChild(doc.createTextNode(img_name+'.jpg')) annotation.appendChild(filename) size = doc.createElement('size') width = doc.createElement('width') width.appendChild(doc.createTextNode(str(imgSize[1]))) size.appendChild(width) height = doc.createElement('height') height.appendChild(doc.createTextNode(str(imgSize[0]))) size.appendChild(height) depth = doc.createElement('depth') depth.appendChild(doc.createTextNode(str(imgSize[2]))) size.appendChild(depth) annotation.appendChild(size) annotation.appendChild(insertObject(doc, datas)) else: annotation.appendChild(insertObject(doc, datas)) try: f.write(doc.toprettyxml(indent=' ')) f.close() fidin.close() except: pass name_size_file.close() if __name__ == '__main__': dataName = 'ICDAR2013' # dataset name
type = 'test' # type
bb_split=' ' img_path = dataName + '/JPEGImages/' + type + '/' # img path
img_name_list_txt = dataName + '/ImageSets/Main/'+type+'.txt' img_list_txt=type+'.txt' create_list(dataName,img_list_txt,img_path,img_name_list_txt,type) labels_path = dataName+'/label/'+type+'/' xmlpath_path = dataName+'/Annotations/'+type+'/' name_size=type+'_name_size.txt'
#txt_to_xml(labels_path,img_path,img_name_list_txt,xmlpath_path,bb_split,name_size)
執行上面的代碼就得到了
A.Annotations/test下的xml格式文件,只要修改type=train就可以得到訓練集的xml格式的gt文件,下同。
B.ImageSets\Main下的test.txt文件
C.執行代碼同級目錄下的test.txt和test_name_size.txt。這兩個文件本應該是用VOCDevit的create_data.sh實現的,此處用python腳本替代了,更方便。注意B和C中的txt文件內容不同,區別如下圖:
四、制作lmdb格式數據。
現在需要的目錄格式是這樣的:(mydataset里面存VOC數據,result里面存轉好的Lmdb格式的數據和通過上述代碼產生的中間結果文件)
所以需要:
1、建立mydataset文件夾,把剛才制作好的VOC整個文件夾丟進去。以后換其他數據庫同樣整個丟進mydataset里面就可以。
2、建立result文件夾,下面建立$dataset_name文件夾,(比如ICDAR2013,跟VOC格式里面的名字一致就可以),並把剛才產生的幾個文件丟進去。
其中的labelmap_ICDAR2013.prototxt是自己建的類別文件,可以仿照VOC0712里面的,如果做文字檢測就只需要兩類,那么內容就如下所示:
item {
name: "none_of_the_above"
label: 0
display_name: "background"
}
item {
name: "text"
label: 1
display_name: "text"
}
3.create_data.sh是VOC0712示例修改過來的,代碼如下:
cur_dir=$(cd $( dirname ${BASH_SOURCE[0]} ) && pwd )
redo=1
#VOC格式數據存放的文件夾
data_root_dir="$cur_dir/mydataset"
#訓練集還是測試集,只是標識一下,就是放在一個文件夾里,放test或者train都是可以的,這樣只是為了方便切換相同數據庫的不同文件夾
type=test
#數據庫名稱,只是標記VOC數據在mydataset下面的哪個文件夾里面,結果又放在哪個文件夾里面。
dataset_name="ICDAR2013"
mapfile="$cur_dir/result/$dataset_name/labelmap_$dataset_name.prototxt"
anno_type="detection"
db="lmdb"
min_dim=0
max_dim=0
width=0
height=0
extra_cmd="--encode-type=jpg --encoded"
if [ $redo ]
then
extra_cmd="$extra_cmd --redo"
fi
for subset in $type
do
#最后一個參數是快捷方式所在的位置,不用建這個文件夾,但是為了代碼改的少參數還是要有,我們在下面的create_annoset.py注釋掉了生成快捷方式那句。
python create_annoset.py --anno-type=$anno_type --label-map-file=$mapfile --min-dim=$min_dim --max-dim=$max_dim --resize-width=$width --resize-height=$height --check-label $extra_cmd $data_root_dir result/$dataset_name/$subset.txt result/$dataset_name/$dataset_name"_"$subset"_"$db result/$dataset_name
done
4、create_annoset.py是在SSD框架的build/tools里面的,為了方便我們直接把它復制過來放在我們當前文件夾下,再稍微修改幾個地方,修改后如下:
import argparse
import os
import shutil
import subprocess
import sys
from caffe.proto import caffe_pb2
from google.protobuf import text_format
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Create AnnotatedDatum database")
parser.add_argument("root",
help="The root directory which contains the images and annotations.")
parser.add_argument("listfile",
help="The file which contains image paths and annotation info.")
parser.add_argument("outdir",
help="The output directory which stores the database file.")
parser.add_argument("exampledir",
help="The directory to store the link of the database files.")
parser.add_argument("--redo", default = False, action = "store_true",
help="Recreate the database.")
parser.add_argument("--anno-type", default = "classification",
help="The type of annotation {classification, detection}.")
parser.add_argument("--label-type", default = "xml",
help="The type of label file format for detection {xml, json, txt}.")
parser.add_argument("--backend", default = "lmdb",
help="The backend {lmdb, leveldb} for storing the result")
parser.add_argument("--check-size", default = False, action = "store_true",
help="Check that all the datum have the same size.")
parser.add_argument("--encode-type", default = "",
help="What type should we encode the image as ('png','jpg',...).")
parser.add_argument("--encoded", default = False, action = "store_true",
help="The encoded image will be save in datum.")
parser.add_argument("--gray", default = False, action = "store_true",
help="Treat images as grayscale ones.")
parser.add_argument("--label-map-file", default = "",
help="A file with LabelMap protobuf message.")
parser.add_argument("--min-dim", default = 0, type = int,
help="Minimum dimension images are resized to.")
parser.add_argument("--max-dim", default = 0, type = int,
help="Maximum dimension images are resized to.")
parser.add_argument("--resize-height", default = 0, type = int,
help="Height images are resized to.")
parser.add_argument("--resize-width", default = 0, type = int,
help="Width images are resized to.")
parser.add_argument("--shuffle", default = False, action = "store_true",
help="Randomly shuffle the order of images and their labels.")
parser.add_argument("--check-label", default = False, action = "store_true",
help="Check that there is no duplicated name/label.")
args = parser.parse_args()
root_dir = args.root
list_file = args.listfile
out_dir = args.outdir
example_dir = args.exampledir
redo = args.redo
anno_type = args.anno_type
label_type = args.label_type
backend = args.backend
check_size = args.check_size
encode_type = args.encode_type
encoded = args.encoded
gray = args.gray
label_map_file = args.label_map_file
min_dim = args.min_dim
max_dim = args.max_dim
resize_height = args.resize_height
resize_width = args.resize_width
shuffle = args.shuffle
check_label = args.check_label
# check if root directory exists
if not os.path.exists(root_dir):
print "root directory: {} does not exist".format(root_dir)
sys.exit()
# add "/" to root directory if needed
if root_dir[-1] != "/":
root_dir += "/"
# check if list file exists
if not os.path.exists(list_file):
print "list file: {} does not exist".format(list_file)
sys.exit()
# check list file format is correct
with open(list_file, "r") as lf:
for line in lf.readlines():
img_file, anno = line.strip("\n").strip("\r").split(" ")
if not os.path.exists(root_dir + img_file):
print "image file: {} does not exist".format(root_dir + img_file)
if anno_type == "classification":
if not anno.isdigit():
print "annotation: {} is not an integer".format(anno)
elif anno_type == "detection":
#print(root_dir + anno)
#print(os.path.exists(root_dir + anno))
if not os.path.exists(root_dir + anno):
print "annofation file: {} does not exist".format(root_dir + anno)
sys.exit()
break
# check if label map file exist
if anno_type == "detection":
if not os.path.exists(label_map_file):
print "label map file: {} does not exist".format(label_map_file)
sys.exit()
label_map = caffe_pb2.LabelMap()
lmf = open(label_map_file, "r")
try:
text_format.Merge(str(lmf.read()), label_map)
except:
print "Cannot parse label map file: {}".format(label_map_file)
sys.exit()
out_parent_dir = os.path.dirname(out_dir)
if not os.path.exists(out_parent_dir):
os.makedirs(out_parent_dir)
if os.path.exists(out_dir) and not redo:
print "{} already exists and I do not hear redo".format(out_dir)
sys.exit()
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
# get caffe root directory
#caffe_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
#print(caffe_root)
caffe_root='/dataL/ljy/caffe-ssd'
if anno_type == "detection":
cmd = "{}/build/tools/convert_annoset" \
" --anno_type={}" \
" --label_type={}" \
" --label_map_file={}" \
" --check_label={}" \
" --min_dim={}" \
" --max_dim={}" \
" --resize_height={}" \
" --resize_width={}" \
" --backend={}" \
" --shuffle={}" \
" --check_size={}" \
" --encode_type={}" \
" --encoded={}" \
" --gray={}" \
" {} {} {}" \
.format(caffe_root, anno_type, label_type, label_map_file, check_label,
min_dim, max_dim, resize_height, resize_width, backend, shuffle,
check_size, encode_type, encoded, gray, root_dir, list_file, out_dir)
elif anno_type == "classification":
cmd = "{}/build/tools/convert_annoset" \
" --anno_type={}" \
" --min_dim={}" \
" --max_dim={}" \
" --resize_height={}" \
" --resize_width={}" \
" --backend={}" \
" --shuffle={}" \
" --check_size={}" \
" --encode_type={}" \
" --encoded={}" \
" --gray={}" \
" {} {} {}" \
.format(caffe_root, anno_type, min_dim, max_dim, resize_height,
resize_width, backend, shuffle, check_size, encode_type, encoded,
gray, root_dir, list_file, out_dir)
print cmd
process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
output = process.communicate()[0]
if not os.path.exists(example_dir):
os.makedirs(example_dir)
link_dir = os.path.join(example_dir, os.path.basename(out_dir))
print(link_dir)
'''
if os.path.exists(link_dir):
os.unlink(link_dir)
os.symlink(out_dir, link_dir)
'''
上面代碼修改的地方是:
A.注釋掉了最后三句。最后三句是創建快捷方式,可以注釋掉。這里不注釋掉會報錯,原因不明,反正也不需要快捷方式,lmdb有了就萬事俱備了。
B.img_file, anno = line.strip("\n").strip("\r").split(" ") ,這句加了("\r")。這句一般情況下改不改都行,但是如果create_voc_data.py是在windows上執行的,后面這個sh在Linux上執行報錯就要改,因為windows和linux系統對換行的處理不同,完全按上述步驟會發現到Linux系統上把換號當回車處理了,導致明明路徑是對的缺找不到相應文件。
C.caffe_root='/dataL/ljy/caffe-ssd'。這句是把caffe目錄切過來。因為原來的代碼是嚴格按照VOC0712數據做的,那么caffe_root就會跟我們不一樣,就需要改。
執行create_data.sh就可以在result/ICDAR2013/下面看到我們得到的lmdb格式的數據了。對於相同數據集只要改type=test或者train就行,不用數據集只要改數據集名字就可以。
五、總結。
從無到有生成目標檢測Lmdb的步驟為:
1. 獲得待制作的圖片
2. 用標記工具標記groundtruth,為txt類型的gt。
3. 按上面的步驟三建立VOC目錄結構並用create_voc_data.py將2中的數據轉為VOC格式。
4. 按上面的步驟四建立結果目錄結構並用create_data.py將3中的數據轉為lmdb格式,完成。
需要注意下面幾點:
1.如何換數據集:只要在上面兩個需要建目錄的地方把ICDAR2013改成其他庫,並把兩個代碼中的dataset_name改成相應數據集名稱就行。
2.如何換相同數據集的的不同部分:比如把ICDAR2013的測試集換成訓練集,只要在相應的目錄下建立train文件夾,並改代碼里面的type=train就可以。