오늘은 AI 허브에서 제공되는 공공데이터를, Nvidia TAO에서 Yolo v4의 훈련데이터로 활용하기 위한 전처리 과정을 기록해볼까 합니다.
몇 달전에 관련 프로젝트로 데이터를 활용한 적이 있어서 코드가 드문드문 기록되어 있었는데, 이 기회에 한번 정리해보겠습니다.
대략적으로 설명드리면 해당 데이터를 다운로드 받아, annotation을 COCO 형식으로 변환한 뒤에 yolo에서 사용 가능한 KITTI 형식으로 재변환 하는 형식으로 진행됩니다.
해당 데이터셋은 용량이 아주 큰 편에 속하지만, 1/3 정도의 데이터셋을 검토하고 이를 총 데이터셋 용량 1/50 정도인 아주 Task Specific한 데이터셋을 만들어보도록 하겠습니다.
위 이미지에서 다운로드 버튼을 누르면 innorixet
라는 툴을 활용해서 데이터를 다운로드 받게 됩니다.
innorixet는 대용량 파일 전송을 위한 툴으로 초당 2~300Mbps의 전송 속도를 보여주네요. (stfp 프로토콜을 이용하네요)
이번 포스팅에서는 [원천]1.공동주택_송도호반.zip
, [원천]1.공동주택_하남감일 A-7BL 11공구 아파트 신축공사.zip
데이터와 [라벨]1.공동주택.zip
을 활용해보겠습니다.
이번 모델은 안전보호구만 탐지하는 모델이기 때문에 레이블에 안전보호구만 들어있는 공동주택 데이터셋 내에 송도호반과 하남강일 데이터셋을 선택하였습니다.
해당 라벨에서 5개의 레이블셋 중에 1.안전보호구만 데이터셋만 사용할 예정입니다.
전부 다운로드 받아 압축을 해제하고 1.안전보호구 관련 데이터만 남기도록 하겠습니다.
그리고 송도호반 데이터셋과 하남강일 데이터셋을 합쳐줍니다.(img는 img끼리, json은 json끼리)
합쳐서 image, label 폴더로 정리하니 image(~~), label(48,277)로 개수가 다릅니다. label에는 image에 포함되지 않은 공동주택_하남감일 A-7BL 11공구 아파트 신축공사 2~11
항목이 추가로 들어가있고, 반대로 기존에 존재했던 레이블 중에서 안전보호구만 빼왔기 때문입니다.
리샘플링하면서 짝(pair)이 누락된 파일을 삭제하도록 하겠습니다.
리샘플링 작업에선 크게 두 가지 작업을 진행할 예정입니다.
일부 데이터셋의 Ground Truth Ratio를 살펴보면 아래와 같습니다.
그래서 이번엔 Ground Truth의 개수인 20000개를 기준으로 최대 클래스 개수가 20000개를 넘어가면 저장되지 않도록 할 예정입니다.
ipynb 파일로 진행하였습니다.
annotation 폴더 내 파일 리스트업
# coding:utf-8
import os
import shutil
import json
import skimage
import numpy as np
import pandas as pd
from collections import OrderedDict
# 원본 annotation 파일들이 있는 경로
annoResampleDir = "/Users/noharam/Desktop/musma/dataset/Training/label/"
# anno_dir 내 annotation 파일 이름 리스트업
annoResampleList = os.listdir(annoResampleDir)
print(len(annoResampleList))
print(annoResampleList[:10])
데이터 프레임 정의
원본의 클래스 개수에 맞게 데이터프레임을 정의합니다.
# DF 정의하기
anno_dict_resample = {"01":0,"02":0,"03":0,"04":0,"05":0,"06":0,"07":0,"08":0,"09":0,"10":0,
"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,
"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,
"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,
"40":0,"41":0,"42":0,"43":0,"44":0,"45":0}
탐지할 클래스를 제외한 다른 클래스를 annotation에서 제거합니다.
동시에 오브젝트 개수도 어느정도 제한합니다.
# 안전보호구 5클래스(1,2,5,7,8)만 남기고 나머지 다 삭제하기
# 수정 후 json 파일 변경
# .DS_Store 파일이 간혹 생길때가 있습니다. 폴더 내에 해당 파일이 존재하면 오류가 발생합니다. 반드시 찾아서 삭제 후 진행하세요.
f = open("/Users/noharam/Desktop/musma/dataset/Training/5class_resampling.txt", 'w')
remove_list = ["03","04","06","09","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25",
"26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45"]
for anno in annoResampleList:
try :
with open(annoResampleDir + anno, encoding="utf-8-sig", errors="ignore") as json_file:
jsonData = json.load(json_file)
except : # .DS_Store가 있는지 확인하기 위해 작성, 일반적으로 mac Finder에는 해당 파일이 보이지 않으나 SFTP 시스템 등으로 보면 해당 파일이 있으니 삭제합니다.
print(anno)
imgPath = anno.replace(".json", ".jpg")
df = pd.json_normalize(jsonData)
filename = df["image.filename"][0]
annoList = df["annotations"][0]
# print(filename)
for i in annoList[:]:
if i["class"] in remove_list:
annoList.remove(i)
if annoList :
for j in range(len(annoList)) :
item = annoList[j]["class"]
# class imblance를 조정하기 위해서 현재는 Tao yolo에서 class_weight를 지원하지만, 애초에 전처리 과정에서 개수를 제한해 적당한 비율 맞추기
# 해당 레이블 개수가 10000개를 이하이면
if anno_dict_resample[item] < 30000 :
anno_dict_resample[item] += 1
# 레이블이 10000개를 넘기지 않을 때만 이미지명을 저장한다. 중복이 생기지만 추후에 제거한다.
f.write(filename+'\n')
with open(annoResampleDir + anno, 'w', encoding="utf-8-sig", errors="ignore") as outfile:
json.dump(jsonData, outfile)
f.close()
결과를 확인해보니 1,2,5,7번 클래스는 오브젝트가 각각 30000개를 달성했지만, 8번 클래스는 5667개밖에 되지 않습니다.
나중에 yolo를 학습시킬 때 class_weight를 8번 클래스에 차등부여하도록 하겠습니다.
print(anno_dict_resample)
# 값(value)를 기준으로 정렬한 OrderedDict
ordered_dict = OrderedDict(sorted(anno_dict_resample.items(), key=lambda t:t[1], reverse=True))
print(ordered_dict)
위 과정 진행 후 텍스트 파일(이미지 리스트) 의 줄 수 확인 : 125667
wc -l 5class_resampling.txt
중복제거는 리눅스 쉘 명령어를 활용해서 진행합니다. : 29598
cat 5class_resampling.txt| sort -u >> 5class_resampling_unique.txt
wc -l 5class_resampling_unique.txt
텍스트 파일 내에서 확장자 문자열(.jpg) 삭제
sed 's/.jpg//' 5class_resampling_unique.txt >> 5class_resampling_nojpg.txt
중복제거 후 나온 리스트를 통해(이미지리스트) 해당 레이블 파일만 뽑아내고복사합니다.
- 전체 레이블 폴더에서 진행해야합니다.
mkdir resampling_label
cd label
cat /Users/noharam/Desktop/musma/dataset/Training/5class_resampling_nojpg.txt | xargs -t -n1 -I{} cp -r {}.json ../resampling_label/
레이블 파일 잘 분류되었는지 폴더 내 파일개수 확인 : 29598
find . -type f | wc -l
('05', 57701), ('01', 56497), ('07', 53839), ('02', 30238), ('08', 5667)
아직 8번 클래스가 05/01/07번 클래스에 비해 1/10정도 밖에 안되네요.
이전엔 위에서 오브젝트 개수를 10,000개로 제한해서 샘플링을 진행했고, 각 클래스별 오브젝트 개수가 비슷한 데이터셋을 얻을 수 있었습니다.
하지만 이번 데이터셋 전처리 목표는 정체되어 있는 훈련에 추가 데이터를 주입하기 위해서라, 너무 큰 불균형을 초래하지 않되 최대한 많은 오브젝트를 담아보려고 했어요.
그래서 08번 클래스와 02번 클래스만 class_weight를 조금 높게 주고 전이학습을 진행해보려고 합니다!
# coding:utf-8
import os
import shutil
import json
import skimage
import numpy as np
import pandas as pd
from collections import OrderedDict
# 중복을 제거하고 남은 이미지에서 다시 한번 클래스의 분포를 확인한다
annoResampleDir = "/Users/noharam/Desktop/musma/dataset/Training/resampling_label/"
# anno_dir 내 annotation 파일 이름 리스트업
annoResampleList = os.listdir(annoResampleDir)
print(len(annoResampleList))
print(annoResampleList[:10])
anno_dict_resample = {"01":0,"02":0,"03":0,"04":0,"05":0,"06":0,"07":0,"08":0,"09":0,"10":0,
"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,
"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,
"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,
"40":0,"41":0,"42":0,"43":0,"44":0,"45":0}
for anno in annoResampleList:
with open(annoResampleDir + anno, encoding="utf-8-sig", errors="ignore") as json_file:
jsonData = json.load(json_file)
imgPath = anno.replace(".json", ".jpg")
df = pd.json_normalize(jsonData)
filename = df["image.filename"][0]
#print(df["image.filename"][0])
for i in range(len(df["annotations"][0])):
item = df["annotations"][0][i]["class"]
anno_dict_resample[item] += 1
# 값(value)를 기준으로 정렬한 OrderedDict
ordered_dict = OrderedDict(sorted(anno_dict_resample.items(), key=lambda t:t[1], reverse=True))
print(ordered_dict)
훈련에 사용할 이미지와 레이블을 image
디렉터리와 resampling_label
디렉터리입니다.
계속 처리를 진행해봅시다.
전 이미지와 레이블 디렉터리를 옮겨주었습니다. 앞으로 image_3
과 label_3
디렉터리로 사용하고자합니다.
mkdir -p /Users/noharam/Desktop/musma/dataset/safety/data/train
mv /Users/noharam/Desktop/musma/dataset/Training/image /Users/noharam/Desktop/musma/dataset/safety/data/train/image_3
mv /Users/noharam/Desktop/musma/dataset/Training/resampling_label /Users/noharam/Desktop/musma/dataset/safety/data/train/label_3
cp -r /Users/noharam/Desktop/musma/dataset/safety/data/train /Users/noharam/Desktop/musma/dataset/safety/data/train_backup
리샘플링이 끝난 후 혹시나 데이터와 레이블의 짝이 맞지 않는 경우가 생깁니다.
짝이 있는지 검사 후 없으면 삭제합니다.
위에서 리샘플링한 레이블 데이터와 마찬가지로 이미지 데이터도 같은 짝을 가진(이미지 파일명이 레이블 데이터와 동일한) 이미지만 남기는 것이 목표입니다.
옮겨놓은 데이터셋 폴더 내에 쉘 파일을 만들고 실행해주세요.
bash clean_dataset_image.sh ./image_3 ./label_3
#!/bin/bash
# 일단 레이블과 쌍이 안맞는 이미지만 체크해서 제거 (전체 이미지 데이터셋에 대해 수행해야함)
for img in $1/*.jpg
do
kitti_name=$2/$(basename $img .jpg).json
if [ ! -f $kitti_name ]; then
echo "$kitti_name not exist, will remove that image pair."
rm $img
fi
done
echo "$1: $(ls $1|wc -l)"
echo "$2: $(ls $2|wc -l)"
echo Done!
처음에 만들어둔 하남강일(1번) + 송도호반 (현재 image_3 디렉터리)에 label_3과 짝이 맞는 이미지만 남겨두고 제거합니다.
- image는 2962개가 남았네요. 계속해서 하남강일 2~11번 데이터셋을 다운로드받고 각각 동일과정을 반복합니다.
하남강일 2번~11번을 순서대로 다운로드 받으면서 같은 과정을 반복합니다.
하남1번+송도호반 : 2,963, 하남 2번: 5,551, 하남 3번: 2,844, 하남 10번:8,697 , 하남 11번: 6,201
장으로 총 26,256장의 이미지 데이터 쌍을 확보했습니다.모든 데이터셋에 대해서 짝 체크가 끝난 이미지만 한 폴더에 모아두고 전체적으로 이미지와 레이블 쌍이 맞는지 다시 한번 체크합니다.
*>_<*
이렇게 전처리가 끝난 후에 날라가는 파일들은 아까워서 말이죠!이럴수가, 이미지를 모두 모으니 레이블개수와 딱 맞는군요. 그래도 검사를 한번 더 진행해주었습니다.
#!/bin/bash
# 이번엔 레이블->이미지 뿐만 아니라, 이미지->레이블 쌍도 검사합니다!
for img in $1/*.jpg
do
kitti_name=$2/$(basename $img .jpg).json
if [ ! -f $kitti_name ]; then
echo "$kitti_name not exist, will remove that image pair."
rm $img
fi
done
for coco in $2/*.json
do
img_name=$1/$(basename $coco .json).jpg
if [ ! -f $img_name ]; then
echo "$img_name not exist, will remove that label pair."
rm $coco
fi
done
echo "$1: $(ls $1|wc -l)"
echo "$2: $(ls $2|wc -l)"
echo Done!
아무 문제가 없다면, 아래처럼 이미지, 레이블 개수만 뜨고, 혹시라도 쌍이 맞지 않는 데이터가 남아있었다면 깔끔하게 제거됩니다!
앞선 과정을 통해 5개의 클래스를 탐지하는 이미지와 레이블 쌍(데이터셋)을 준비하였습니다.
이제 레이블 파일들(json)을 하나의 COCO annotation 파일로 만들어보겠습니다.
# coding:utf-8
import os
import shutil
import json
# 코드를 실행하기 전에 이미지-json 파일이 정확한 쌍을 이루는지 먼저 확인해주세요.
# 원본 annotation 파일들이 있는 경로
annoDir = "/Users/noharam/Desktop/musma/dataset/safety/data/train/label_3/"
# image가 있는 경로
imageDir = "/Users/noharam/Desktop/musma/dataset/safety/data/train/image_3/"
# anno_dir 내 annotation 파일 이름 리스트업
annoList = os.listdir(annoDir)
print(len(annoList))
# cocoDict에 필요한 images, annotations 정보들의 리스트
imgTmpDict = []
annoTmpDict = []
idNum = 1
annoIdNum = 60000
for anno in annoList:
with open(annoDir + anno, encoding="utf-8-sig", errors="ignore") as json_file:
jsonData = json.load(json_file)
imgPath = anno.replace(".json", ".jpg")
imgSize = 1920 * 1080
try :
images = {
"license": 1,
"file_name": jsonData["image"]["filename"],
"coco_url": "",
"height": jsonData["image"]["resolution"][1],
"width": jsonData["image"]["resolution"][0],
"date_captured": jsonData["image"]["date"],
"flickr_url": "",
"id": idNum
}
except :
images = {
"license": 1,
"file_name": jsonData["image"]["filename"],
"coco_url": "",
"width": 1920,
"height": 1080,
"date_captured": jsonData["image"]["date"],
"flickr_url": "",
"id": idNum
}
imgTmpDict.append(images)
for i in range(len(jsonData["annotations"])):
try: # bbox 정보가 존재할 경우
bbox = [
jsonData["annotations"][i]["box"][0],
jsonData["annotations"][i]["box"][1],
jsonData["annotations"][i]["box"][2]-jsonData["annotations"][i]["box"][0],
jsonData["annotations"][i]["box"][3]-jsonData["annotations"][i]["box"][1]
]
annotation = {
"segmentation": [],
"area": "",
"iscrowd": "",
"image_id": idNum,
"bbox": bbox,
"category_id": int(jsonData["annotations"][i]["class"]),
"id": annoIdNum,
"flags": jsonData["annotations"][i]["flags"],
}
except: # polygon 정보가 존재할 경우
annotation = {
"segmentation": jsonData["annotations"][i]["polygon"],
"area": "",
"iscrowd": "",
"image_id": idNum,
"bbox": [0,0,0,0],
"category_id": int(jsonData["annotations"][i]["class"]),
"id": annoIdNum,
"flags": jsonData["annotations"][i]["flags"],
}
annoTmpDict.append(annotation)
annoIdNum += 1
idNum += 1
# COCO format annotation이 저장될 dict
cocoDict = {}
cocoDict["info"] = {
"description": "AI Hub 공사현장 안정장비 인식 데이터셋",
"url": "https://aihub.or.kr/aidata/33921",
"version": "1.0",
"year": 2021,
"contributor": "Haram roh",
"date_created": "2021/07/22",
}
cocoDict["licenses"] = [
{"url": "https://aihub.or.kr/aidata/33921", "id": 1, "name": "미디어그룹사람과숲(컨)"}
]
cocoDict["images"] = imgTmpDict
cocoDict["annotations"] = annoTmpDict
cocoDict["categories"] = [
{"supercategory": "S2", "id": 1, "name": "안전벨트 착용"},
{"supercategory": "S2", "id": 2, "name": "안전벨트 미착용"},
{"supercategory": "S2", "id": 5, "name": "안전화 착용"},
{"supercategory": "S2", "id": 7, "name": "안전모 착용"},
{"supercategory": "S2", "id": 8, "name": "안전모 미착용"},
]
처리가 완료된 후 파일을 저장합니다.
with open("/Users/noharam/Desktop/musma/dataset/safety/data/train/5class_coco.json", 'w') as f :
json.dump(cocoDict, f, ensure_ascii=False)
print("완료했습니다.")
이번엔 COCO Dataset 형식을 KITTI Dataset 형식으로 변환해줍니다.
yolo_v4에서 데이터셋 포맷으로 KITTI 혹은 KITTI를 TFRecords로 변환한 데이터셋을 사용하기 때문입니다.
# -*- coding: utf-8 -*-
"""coco2kitti.py: Converts MS COCO annotation files to
Kitti format bounding box label files
__author__ = "haram roh"
"""
import os
from pycocotools.coco import COCO
def coco2kitti(catNms, annFile):
# initialize COCO api for instance annotations
coco = COCO(annFile)
# Create an index for the category names
cats = coco.loadCats(coco.getCatIds())
cat_idx = {}
for c in cats:
cat_idx[c['id']] = c['name']
for img in coco.imgs:
# Get all annotation IDs for the image
catIds = coco.getCatIds(catNms=catNms)
annIds = coco.getAnnIds(imgIds=[img], catIds=catIds) # Image id, Category id를 input으로, 그에 해당하는 annotation id를 return 하는 함수
# If there are annotations, create a label file
if len(annIds) > 0:
# Get image filename
img_fname = coco.imgs[img]['file_name']
# open text file
with open('./labels/' + img_fname.split('.')[0] + '.txt','w') as label_file:
anns = coco.loadAnns(annIds)
for a in anns:
if a['bbox'] :
bbox = a['bbox']
# Convert COCO bbox coords to Kitti ones
bbox = [bbox[0], bbox[1], bbox[2] + bbox[0], bbox[3] + bbox[1]]
bbox = [str(b) for b in bbox]
catname = cat_idx[int(a['category_id'])]
# Format line in label file
# Note: all whitespace will be removed from class names
out_str = [catname.replace(" ","")
+ ' ' + ' '.join(['0']*3)
+ ' ' + ' '.join([b for b in bbox])
+ ' ' + ' '.join(['0']*8)
+'\n']
label_file.write(out_str[0])
if __name__ == '__main__':
# These settings assume this script is in the annotations directory
dataDir = "/Users/noharam/Desktop/musma/dataset/safety/data/train"
dataType = "5class_coco"
annFile = '%s/%s.json' % (dataDir, dataType)
catNms = [
"안전벨트 착용", "안전벨트 미착용", "안전화 착용", "안전모 착용", "안전모 미착용"
]
if os.path.isdir('./labels'):
print('Labels folder already exists - exiting to prevent badness')
else:
os.mkdir('./labels')
coco2kitti(catNms, annFile)
해당 과정 수행 후 코드를 실행한 폴더에 내부에 labels
디렉토리가 생성되고, 그 안에 KITTI dataset annotation인 txt 파일들이 들어가게 됩니다.
당연히 파일 개수는 원본 레이블(전처리 후)와 같게 25,599개입니다.
자, 이제 KITTI 형식으로 읽을 수 있는 이미지와 레이블 데이터셋 준비가 모두 끝났습니다.
저는 이 데이터셋을 SFTP로 사내 서버로 옮겨, NVIDIA TAO에서 yolo_v4 훈련에 활용합니다.
이번 포스팅은 TAO 관련 Object Detection과 이를 AIoT 기기에서 활용하기 위한 주제가 아니라, 모델 훈련을 위한 공공데이터 전처리에 대한 내용이기 때문에 여기서 포스팅을 마무리합니다.
고생하셨습니다!
해당 데이터셋을 활용하시는 분을 위한 추가정보입니다.
[원천]3.공장_가산동 A1타워 지식산업 센터2.zip
과 같은 데이터셋의 레이블에는 아래처럼 전체 레이블이 아닌 구조물_비계만, 자재및공구만, 전체
등이 저장되어 있습니다.
위에서 활용하려는 일부 레이블이 없는 데이터셋이 존재하기 때문에 사용하려는 레이블이 있는 데이터셋인지 라벨 파일 먼저 다운로드 후 체크해보시기 바랍니다.
Tao를 이용한 Yolo_v4 전이학습, Pruning, Retrain, 양자화, TensorRT 등의 NVIDIA TAO 관련 내용은 추후에 포스팅 할 계획입니다.