가장 많이 사용되어지는 Annotation Tools 인것 같으며, 저장되는 형식은 XML이며 Pascal VOC 와 YOLO로 구분되어 저장가능하다.
LabelImg 설치방법 및 소스
https://github.com/tzutalin/labelImg
- LabelImg Download 및 필요 Package 설치
$ cd /works/custom $ git clone https://github.com/tzutalin/labelImg.git $ cd labelImg
$ sudo apt install pyqt5-dev-tools $ sudo pip3 install -r requirements/requirements-linux-python3.txt $ make qt5py3
소스기반으로 설치한 이유는 직접소스에서 HotKey를 변경하기 위해서 상위와 같이 설치
만약 쉽게설치하고 싶다면 pip install 로도 설치가능
참고
https://eehoeskrap.tistory.com/331
- Source 에서 직접 아래와 같이 HotKey 변경
소스를 보면 python으로 작성이 되어있어 쉽게 이해가능하므로, 소스로 설치하여 본인이 원하는 곳을 고치자.
$ vi labelImg.py 212 openNextImg = action(getStr('nextImg'), self.openNextImg, 213 # 'd', 'next', getStr('nextImgDetail')) 214 'f', 'next', getStr('nextImgDetail')) .... 216 openPrevImg = action(getStr('prevImg'), self.openPrevImg, 217 # 'a', 'prev', getStr('prevImgDetail')) 218 's', 'prev', getStr('prevImgDetail')) .... 223 save = action(getStr('save'), self.saveFile, 224 # 'Ctrl+S', 'save', getStr('saveDetail'), enabled=False) 225 'a', 'save', getStr('saveDetail'), enabled=False) .... 240 createMode = action(getStr('crtBox'), self.setCreateMode, 241 # 'w', 'new', getStr('crtBoxDetail'), enabled=False) 242 'e', 'new', getStr('crtBoxDetail'), enabled=False) .. 246 create = action(getStr('crtBox'), self.createShape, 247 # 'w', 'new', getStr('crtBoxDetail'), enabled=False) 248 'e', 'new', getStr('crtBoxDetail'), enabled=False)
Hotkey
w Create a rect box : e 변경
d Next image : f 변경
a Previous image : s 변경
Ctrl + s Save : a 변경
상위와 같이 변경한 이유는 한 손에 전부 넣어 빨리 편집하기위해서 Hot Key를 변경
- LabelImg 실행 (Args는 옵션)
2nd Arg : Annotation 할때 붙여지는 Class 정의된 File을 직접 선택가능
$ python3 labelImg.py
$ python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
- 실행하면 좌측에 메뉴
- OpenDir : Image 위치설정
- Change Save Dir : Xml 저장위치설정
실행시 이전에 저장되어진 Xml 저장위치 기준으로 XML를 가져와서 BBOX를 표시를 해준다.
https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/training.html#annotating-images
1.1 파일이름 변경 방법
본인이 원하는 DATA Image들을 다운을 받고 여러 파일들을 한꺼 번에 이름을 변경하고자 할때 많을 것 같다.
아래와 같이 rename 명령어를 사용하거나 간단하게 Shell script를 이용하여 만들어서 이를 해결하자
- rename 명령어를 이용하여 파일이름 변경
$ ls image_01.jpg image_02.jpg image_10.jpg $ rename 's/image/image_test/' *.jpg image_test_01.jpg image_test_02.jpg image_test_10.jpg
rename 명령어로 특정 Pattern이 있는 것을 찾아 이름을 변경을 해보자.
만약 특정 Pattern이 없다면 아래와 같이 Shell Script를 사용해서 본인이 원하는 대로 변경하자
- rename.sh shell script 작성(rename 명령어로 한계가 있어 Shell Script 직접작성함)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# rename jpg files in sequence with same pattern | |
# | |
# Author : Jeonghun Lee | |
# Version : 0.1 | |
# | |
# ./rename.sh | |
# or | |
# ./rename.sh 1stArg 2ndArg | |
CNT=0 | |
PREFIX=${1:-"image_"} | |
POSTFIX=${2:-".jpg"} | |
echo -e "\e[91mStart: rename all jpg files to ${PREFIX}x${POSTFIX}\e[39m\n" | |
for FILE in *$POSTFIX | |
do | |
NAME=${PREFIX}${CNT}${POSTFIX} | |
echo -e "ORG:$FILE \e[34m NEW:$NAME \e[39m Index:$CNT" | |
mv $FILE $NAME | |
CNT=$((CNT+1)) | |
done | |
echo -e "\e[91mfinished \e[39m\n" | |
- rename.sh shell script 실행
$ cd ~/works/custom/data/images $ chmod +x rename.sh $ ./rename.sh // *.jpg 파일들을 찾아 image_xx.jpg로 변경 or $ ./rename.sh Image_data .png // *.png 파일들을 찾아 image_data_xx.png 로 변경
1.2 DATA SET 구성 과 Annotation 진행
- DATA SET 구성
$ cd ~/works/fire/data $ mkdir -p images/train $ mkdir -p images/test $ mkdir -p annotation/train $ mkdir -p annotation/test $ tree -L 2 . ├── annotation │ ├── test // test xml (pascal voc type) │ └── train // train xml (pascal voc type) └── images ├── test // test image (eval) └── train // train image
- LabelImg class 정의
$ cd /works/custom/labelImg $ vi data/fire_classes.txt fire smoke
주의
상위 정의된 이름과 label_map.pbtxt에 정의된 이름이 완전히 동일해야 한다.
XML에서 이름만 가지고 찾아 찾기 때문에
- labelImg (train)
좌측 Change Save Dir 로 XML 저장장소 ~/work/fire/data/annotation/train 변경
$ python3 labelImg.py ~/works/fire/data/images/train data/fire_classes.txt
- labelImg (test)
좌측 Change Save Dir 로 XML 저장장소 ~/work/fire/data/annotation/test 변경
변경을 하자마자 바로 적용이 안되므로 Prev Image / Next Image 로 Refresh
$ python3 labelImg.py ~/works/fire/data/images/test data/fire_classes.txt
1.3 label map file 생성 및 정의
본인이 원하는 item을 정하여 각각의 name id를 정의해서 넣자
- lable map 만들기
$ cd ~/works/fire/data // 상위 이름과 동일 $ vi label_map.pbtxt item { id: 1 name: 'fire' } item { id: 2 name: 'smoke' }
https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/training.html#creating-label-map
1.4 TF Record File 변환
TF Record 는 기본으로 Tensorflow가 설치가 되어야 가능하므로 이전에 설치진행 혹은 Docker에서 진행
Tensorflow Manual
https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/training.html#creating-tensorflow-records
- Tensorflow 실행 및 준비
$ docker run --gpus all --rm -it \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ -p 8888:8888 -p 6006:6006 \ -v /home/jhlee/works/fire/data:/data \ --ipc=host \ --name nvidia_ssd \ nvidia_ssd root@3aac229c45c3:/workdir/models/research# pip install lxml //XML를 위해 필요 root@3aac229c45c3:/workdir/models/research# vi create_pascal_tf_record.py //아래의 소스로 작성
- Pascal SET 을 TF Record 생성
왜냐하면 아래의 소스를 보면 --data_dir 과 XML의 folder로 찾아 넣는다.
root@3aac229c45c3:/workdir/models/research# python create_pascal_tf_record.py \ --data_dir=/data/images \ --annotations_dir=/data/annotation/train \ --label_map_path=/data/label_map.pbtxt \ --output_path=/data/train.record /workdir/models/research/object_detection/utils/dataset_util.py:75: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. if not xml: root@3aac229c45c3:/workdir/models/research# python create_pascal_tf_record.py \ --data_dir=/data/images \ --annotations_dir=/data/annotation/test \ --label_map_path=/data/label_map.pbtxt \ --output_path=/data/test.record /workdir/models/research/object_detection/utils/dataset_util.py:75: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. if not xml:
2. create_pascal_tf_record.py 소스 분석
create_pascal_tf_record.py를 간단히 분석을 해보면, XML 기반으로 JPEG Image를 넣어 TF Record를 만들어 넣는다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright 2017 The TensorFlow Authors. All Rights Reserved. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
# ============================================================================== | |
r"""Convert raw PASCAL dataset to TFRecord for object_detection. | |
Example usage: | |
python object_detection/dataset_tools/create_pascal_tf_record.py \ | |
--data_dir=/home/user/VOCdevkit \ | |
--output_path=/home/user/pascal.record | |
--label_map_path=/home/user/dataset/label.pbtxt | |
""" | |
from __future__ import absolute_import | |
from __future__ import division | |
from __future__ import print_function | |
import hashlib | |
import io | |
import logging | |
import os | |
from lxml import etree | |
import PIL.Image | |
import tensorflow as tf | |
from object_detection.utils import dataset_util | |
from object_detection.utils import label_map_util | |
flags = tf.app.flags | |
flags.DEFINE_string('data_dir', '', 'Root directory to raw PASCAL VOC dataset.') | |
flags.DEFINE_string('set', 'train', 'Convert training set, validation set or ' | |
'merged set.') | |
flags.DEFINE_string('annotations_dir', 'Annotations', | |
'(Relative) path to annotations directory.') | |
flags.DEFINE_string('output_path', '', 'Path to output TFRecord') | |
flags.DEFINE_string('label_map_path', 'data/pascal_label_map.pbtxt', | |
'Path to label map proto') | |
flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore ' | |
'difficult instances') | |
FLAGS = flags.FLAGS | |
SETS = ['train', 'val', 'trainval', 'test'] | |
def dict_to_tf_example(data, | |
dataset_directory, | |
label_map_dict, | |
ignore_difficult_instances=False): | |
"""Convert XML derived dict to tf.Example proto. | |
Notice that this function normalizes the bounding box coordinates provided | |
by the raw data. | |
Args: | |
data: dict holding PASCAL XML fields for a single image (obtained by | |
running dataset_util.recursive_parse_xml_to_dict) | |
dataset_directory: Path to root directory holding PASCAL dataset | |
label_map_dict: A map from string label names to integers ids. | |
ignore_difficult_instances: Whether to skip difficult instances in the | |
dataset (default: False). | |
Returns: | |
example: The converted tf.Example. | |
Raises: | |
ValueError: if the image pointed to by data['filename'] is not a valid JPEG | |
""" | |
img_path = os.path.join(data['folder'], data['filename']) ## Image Path --data_dir /XML의 folder/filename | |
full_path = os.path.join(dataset_directory, img_path) | |
with tf.gfile.GFile(full_path, 'rb') as fid: | |
encoded_jpg = fid.read() | |
encoded_jpg_io = io.BytesIO(encoded_jpg) | |
image = PIL.Image.open(encoded_jpg_io) | |
if image.format != 'JPEG': ## JPEG만 가능 | |
raise ValueError('Image format not JPEG') | |
key = hashlib.sha256(encoded_jpg).hexdigest() | |
width = int(data['size']['width']) ## XML Information | |
height = int(data['size']['height']) | |
xmin = [] | |
ymin = [] | |
xmax = [] | |
ymax = [] | |
classes = [] | |
classes_text = [] | |
truncated = [] | |
poses = [] | |
difficult_obj = [] | |
if 'object' in data: | |
for obj in data['object']: #XML의 object | |
difficult = bool(int(obj['difficult'])) #XML의 difficult | |
if ignore_difficult_instances and difficult: | |
continue | |
difficult_obj.append(int(difficult)) | |
xmin.append(float(obj['bndbox']['xmin']) / width) ## XML information | |
ymin.append(float(obj['bndbox']['ymin']) / height) | |
xmax.append(float(obj['bndbox']['xmax']) / width) | |
ymax.append(float(obj['bndbox']['ymax']) / height) | |
classes_text.append(obj['name'].encode('utf8')) | |
classes.append(label_map_dict[obj['name']]) | |
truncated.append(int(obj['truncated'])) | |
poses.append(obj['pose'].encode('utf8')) | |
example = tf.train.Example(features=tf.train.Features(feature={ | |
'image/height': dataset_util.int64_feature(height), | |
'image/width': dataset_util.int64_feature(width), | |
'image/filename': dataset_util.bytes_feature( | |
data['filename'].encode('utf8')), | |
'image/source_id': dataset_util.bytes_feature( | |
data['filename'].encode('utf8')), | |
'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')), | |
'image/encoded': dataset_util.bytes_feature(encoded_jpg), | |
'image/format': dataset_util.bytes_feature('jpeg'.encode('utf8')), | |
'image/object/bbox/xmin': dataset_util.float_list_feature(xmin), | |
'image/object/bbox/xmax': dataset_util.float_list_feature(xmax), | |
'image/object/bbox/ymin': dataset_util.float_list_feature(ymin), | |
'image/object/bbox/ymax': dataset_util.float_list_feature(ymax), | |
'image/object/class/text': dataset_util.bytes_list_feature(classes_text), | |
'image/object/class/label': dataset_util.int64_list_feature(classes), | |
'image/object/difficult': dataset_util.int64_list_feature(difficult_obj), | |
'image/object/truncated': dataset_util.int64_list_feature(truncated), | |
'image/object/view': dataset_util.bytes_list_feature(poses), | |
})) | |
return example | |
def main(_): | |
if FLAGS.set not in SETS: | |
raise ValueError('set must be in : {}'.format(SETS)) | |
data_dir = FLAGS.data_dir ## --data_dir= image의 앞 directory , image 는 --data_dir 과 XML의 folder 이용 | |
writer = tf.python_io.TFRecordWriter(FLAGS.output_path) | |
label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path) | |
annotations_dir = os.path.join(data_dir, FLAGS.annotations_dir) | |
## FLAGS.annotations_dir --annotations_dir= 절대 PATH 설정하면 뒤의 설정으로 Join가능 | |
examples_list = os.listdir(annotations_dir) | |
for el in examples_list: | |
if el[-3:] !='xml': | |
del examples_list[examples_list.index(el)] | |
for el in examples_list: | |
examples_list[examples_list.index(el)] = el[0:-4] | |
for idx, example in enumerate(examples_list): | |
if idx % 100 == 0: | |
logging.info('On image %d of %d', idx, len(examples_list)) | |
path = os.path.join(annotations_dir, example + '.xml') | |
with tf.gfile.GFile(path, 'r') as fid: | |
xml_str = fid.read() | |
xml = etree.fromstring(xml_str) | |
data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation'] ## XML annotation Parsing | |
tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict, | |
FLAGS.ignore_difficult_instances) | |
writer.write(tf_example.SerializeToString()) | |
writer.close() | |
if __name__ == '__main__': | |
tf.app.run() |