Commit 88544226 authored by Gilberto Astolfi's avatar Gilberto Astolfi
Browse files

Primeiro envio

parent e34d416e
# crossvalidation-repetition-object-detection
# Responsáveis:
Gilberto Astolfi (gilbertoastolfi@gmail.com)
# Resumo:
Constrói dois tipos de amostragem para treinar a Faster-RCNN: validação cruzada e repetições. O projeto foi feito para ser usado no Google Colab. Para usá-lo faça o upload do arquivo crossvalidation_augmentation_oversampling_for_object_detection.ipynb em seu Google Colab e siga as instruções contidas neste notebook.
import random
import os
import operator
import xml.etree.ElementTree as et
import cv2
from transformation_image import Transformation
import argparse
class DataAugmentation:
def __init__(self, path_folds, percentage):
self.root_path = path_folds
self.percentage = percentage
self.transformation = Transformation()
self.idx_continue = 0
def readImage(self, xml_file ):
root = et.parse(xml_file).getroot()
image_path = root.find("path").text
image = cv2.imread(image_path, cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
return image
def pathImageFromXml(self, xml_file):
root = et.parse(xml_file).getroot()
image_path = root.find("path").text
return image_path
def listXmlFromClass(self, path):
files_imgs = []
files = os.listdir(path)
for file in files:
name, extension = os.path.splitext(file)
if extension.lower() == '.xml'.lower():
file_name = path + '/' + file
name_splited = file_name.split('/')
name_image = name_splited[len(name_splited)-1]
if '_aug_' not in name_image and '_over_' not in name_image:
files_imgs.append(file_name)
return files_imgs
def listBoundingBoxImage(self, xml_file):
root = et.parse(xml_file).getroot()
bndbox = []
for obj in root.findall('object'):
class_name = obj.find('name').text
xmin = int(obj.find('bndbox').find('xmin').text)
ymin = int(obj.find('bndbox').find('ymin').text)
xmax = int(obj.find('bndbox').find('xmax').text)
ymax = int(obj.find('bndbox').find('ymax').text)
bndbox.append([xmin,ymin,xmax,ymax,class_name])
return bndbox
def createFileXml(self, filename, size, classe, bndbox, path_xml, path):
out_xml = '<annotation>\n'
out_xml += '\t<folder>img</folder>\n'
out_xml += '\t<filename>'+filename+'</filename>\n'
out_xml += '\t<path>'+path+'</path>\n'
out_xml += '\t<source>\n'
out_xml += '\t\t<database>Unknown</database>\n'
out_xml += '\t</source>\n'
out_xml += '\t<size>\n'
out_xml += '\t\t<width>'+str(size[1])+'</width>\n'
out_xml += '\t\t<height>'+str(size[0])+'</height>\n'
out_xml += '\t\t<depth>'+str(size[2])+'</depth>\n'
out_xml += '\t</size>\n'
out_xml += '\t<segmented>0</segmented>\n'
out_xml += '\t<object>\n'
out_xml += '\t\t<name>'+classe+'</name>\n'
out_xml += '\t\t<pose>Unspecified</pose>\n'
out_xml += '\t\t<truncated>0</truncated>\n'
out_xml += '\t\t<difficult>0</difficult>\n'
out_xml += '\t\t<bndbox>\n'
out_xml += '\t\t\t<xmin>'+str(bndbox[0])+'</xmin>\n'
out_xml += '\t\t\t<ymin>'+str(bndbox[1])+'</ymin>\n'
out_xml += '\t\t\t<xmax>'+str(bndbox[2])+'</xmax>\n'
out_xml += '\t\t\t<ymax>'+str(bndbox[3])+'</ymax>\n'
out_xml += '\t\t</bndbox>\n'
out_xml += '\t</object>\n'
out_xml += '</annotation>'
fo = open(path_xml, "w")
fo.write(out_xml)
fo.close()
def saveImageAnnotation(self, rotated_images, src, label):
file_name, _ = os.path.splitext(src)# path do arquivo original sem extensão. Exemplo: path/path/inseto1
num_images = 1
for ri in rotated_images: #(cl, rotated_image, new_boundingbox)
path_image = file_name + label +str(num_images)+'.jpg' # tag path e para salvar o arquivo jpg
path_xml = file_name + label +str(num_images)+'.xml' # para salvar o arquivo xml
if os.path.isfile(path_xml):
print ("-----------------> File exist")
name_splited = path_image.split('/')
filename = name_splited[len(name_splited)-1] #tag filename. Exemplo: inseto1.jpg
size = ri[1].shape
classe = ri[0]
bndbox = ri[2]
self.createFileXml(filename, size, classe, bndbox, path_xml, path_image)
image = ri[1]
img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
cv2.imwrite(path_image, img)
num_images = num_images + 1
def applyAugmentationInDir(self, list_xml, num_image):
for idx in range(num_image):
#idx = random.randint(0, len(list_xml)-1) # get a xml file randomly
xml = list_xml[idx]
original_image = self.readImage(xml)
list_bb = self.listBoundingBoxImage(xml)
path_image = self.pathImageFromXml(xml)
trans_images = [] # conjunto de imagens geradas para cada bb do xml
# cria uma imagem para cada bb
for bb_ in list_bb:
bb = bb_[0:4]
cl = bb_[4]
rotated_images = self.transformation.applyTransformation(original_image, bb, "rotation", 1)
rotated_image = rotated_images[0][0]
new_boundingbox = rotated_images[0][1]
trans_images.append((cl, rotated_image, new_boundingbox))
self.saveImageAnnotation(trans_images, path_image, '_aug_')
def applyAugmentationInSet(self, path, classes):
for cl in classes:
path_class = path + '/' + cl
list_xml = self.listXmlFromClass(path_class)
num_image = int(len(list_xml) * self.percentage) # Calculates the number of images to be generated
self.applyAugmentationInDir(list_xml, num_image)
def augmentation(self):
print('Iniciando aumento de dados...')
folds = os.listdir(self.root_path)
for fold in folds:
path_train = self.root_path + '/' + fold + '/train'
path_validation = self.root_path + '/' + fold + '/validation'
classes_train = os.listdir(path_train)
classes_validation = os.listdir(path_validation)
self.applyAugmentationInSet(path_train, classes_train)
self.applyAugmentationInSet(path_validation, classes_validation)
print('Fim aumento de dados...')
def countClass(self, xml_file):
root = et.parse(xml_file).getroot()
num_classes = {}
for obj in root.findall('object'):
class_name = obj.find('name').text
if class_name in num_classes:
num_classes[class_name] +=1
else:
num_classes[class_name] =1
return num_classes
def countClassDir(self, path):
num_classes = {}
classes = os.listdir(path)
for cl in classes:
path_class = path + '/' + cl
files = os.listdir(path_class)
for file in files:
name, extension = os.path.splitext(file)
if extension.lower() == '.xml'.lower():
file_name = path_class + '/' + file
n_classes = self.countClass(file_name)
for class_name, number in n_classes.items():
if class_name in num_classes:
n = num_classes.get(class_name)
num_classes[class_name] = (number + n)
else:
num_classes[class_name] = number
return num_classes # exemplo -> {'edessa_body': 39, 'edessa_head': 39, 'edessa_paw': 204}
#verifica se a classe está no xml
def hasClass(self, xml_file, classe):
root = et.parse(xml_file).getroot()
for obj in root.findall('object'):
class_name = obj.find('name').text
if class_name == classe:
return True
return False
#lista todos os xml que tem a classe em questão
def listAllXmlFromClass(self, path, classe):
xml_with_class = []
classes = os.listdir(path)
for cl in classes:
path_class = path + '/' + cl
files = self.listXmlFromClass(path_class)
for file in files:
if self.hasClass(file, classe):
xml_with_class.append(file)
return xml_with_class
def listBoundingBoxFromClassInImage(self, xml_file, classe):
root = et.parse(xml_file).getroot()
bndbox = []
for obj in root.findall('object'):
class_name = obj.find('name').text
if class_name == classe:
xmin = int(obj.find('bndbox').find('xmin').text)
ymin = int(obj.find('bndbox').find('ymin').text)
xmax = int(obj.find('bndbox').find('xmax').text)
ymax = int(obj.find('bndbox').find('ymax').text)
bndbox.append([xmin,ymin,xmax,ymax,class_name])
return bndbox
# path: train ou validation
# quant: número de anotações da classe
def applyOversamplingInClass(self, path, num_gerar, classe):
list_xml = self.listAllXmlFromClass(path, classe) # lista todos os xml que tem pelo menos uma anotação da classe
for over in range(num_gerar):
idx = random.randint(0, len(list_xml)-1) #gera um indice aleatóriamente
xml = list_xml[idx] # pega um xml aleatóriamente
original_image = self.readImage(xml)
#lista somente os bb da classe
list_bb_from_class = self.listBoundingBoxFromClassInImage(xml, classe)
idxbb = 0
if len(list_bb_from_class) > 1:
#gera um indice aleatóriamente
idxbb = random.randint(0, len(list_bb_from_class)-1) #
bb_ = list_bb_from_class[idxbb] #pega um bb aleatóriamente da lista
bb = bb_[0:4] # bb
cl = bb_[4] # classe
rotated_images = self.transformation.applyTransformation(original_image, bb, "rotation", 1)
rotated_image = rotated_images[0][0]
new_boundingbox = rotated_images[0][1]
trans_images = [] # conjunto de imagens geradas para cada bb do xml
trans_images.append((cl, rotated_image, new_boundingbox))
path_image = self.pathImageFromXml(xml)
label = '_over_'+str(self.idx_continue)+'_'
self.idx_continue += 1
self.saveImageAnnotation(trans_images, path_image,label)
def applyOversamplingInSet(self, path, classes):
num_classes = self.countClassDir(path)
max_value = max(num_classes.values()) # indica a classe que tem maior número de anotações
#percorre as classes e obtem o numero de anotações que cada uma passui
for cl, num_anot in num_classes.items():
num_gerar = max_value - num_anot # número de imagens a serem geradas
#print(cl,' ---> ',num_gerar, ' -- ', num_gerar+num_anot)
if num_gerar == 0:
continue
else:
# gerar num_gerar de imagens para a classe cl
self.applyOversamplingInClass(path, num_gerar, cl)
def oversampling(self):
print('Iniciando oversampling...')
folds = os.listdir(self.root_path)
for fold in folds:
path_train = self.root_path + '/' + fold + '/train'
path_validation = self.root_path + '/' + fold + '/validation'
classes_train = os.listdir(path_train)
classes_validation = os.listdir(path_validation)
#print(10*'**',fold)
#print(self.countClassDir(path_train))
self.applyOversamplingInSet(path_train, classes_train)
#print(self.countClassDir(path_train))
#print(15*'--')
#print(self.countClassDir(path_validation))
self.applyOversamplingInSet(path_validation, classes_validation)
#print(self.countClassDir(path_validation))
print('Fim oversampling...')
if __name__ == '__main__':
path_out = './data/folds'
dt = DataAugmentation(path_out, .3)
dt.augmentation()
dt.oversampling()
\ No newline at end of file
import os
from sklearn.model_selection import ShuffleSplit
from shutil import copyfile
import numpy as np
import cv2
import shutil
import xml.etree.ElementTree as ET
class CrossValidation:
def __init__(self, path_out, path, number_fold, validation):
self.path = path
self.path_out = path_out
self.number_fold = number_fold
self.validation = validation
def get_path(self):
return self.path
def get_path_out(self):
return self.path_out
def listdir_nohidden(self,path):
folders = []
for f in os.listdir(path):
content = path +'/'+f
if os.path.isdir(content):
if not f.startswith('.'):
folders.append(f)
return folders
def __listDir(self, path):
if os.path.exists(path):
return self.listdir_nohidden(path)
else:
return None
def __listImages(self, path):
files_imgs = []
files = os.listdir(path)
for file in files:
name, extension = os.path.splitext(file)
if extension.lower() == '.jpg'.lower() or extension.lower() == '.png'.lower():
file_name = path + file
files_imgs.append(file_name)
return files_imgs
def createDirectory(self, directory):
if not os.path.exists(directory):
os.makedirs(directory)
def readImage(self, path):
'''
Read image of the path
:param path: path to image
:return: image
'''
img = cv2.imread(path)
return img
def read_dataset(self):
'''
Retorna um dictionary onde key é a classe e o value é uma lista com os nomes de aruivos de imagens
exemplo: {
'soja_doente': ['img1.jpg','img2.jpg','img3.jpg'],
'soja_sadia': ['img4.jpg','img5.jpg','img6.jpg']
}
:param path: path to image
:return: image
'''
images_dict = {}
list_class = self.__listDir(self.get_path()) # lista todas as pastas, classes
if list_class is not None:
for cl in list_class:
list_images = []
images = self.__listImages(self.get_path() + '/' + cl + '/')
for img in images: # para cada classe, pega todos os nomes de arquivo de imagens
list_images.append(img) # adiciona cada nome de arquivo de imagem em uma lista
images_dict[cl] = list_images # key -> o nome da classe, value -> a lista com o nomes dos arquivos da classe
return images_dict
def copy_files(self, src, dst):
name, _ = os.path.splitext(src)
src_xml = name + '.xml'
src_txt = name + '.txt'
name, _ = os.path.splitext(dst)
path_img = name + '.jpg'
dst_xml = name + '.xml'
dst_txt = name + '.txt'
copyfile(src_xml, dst_xml)
copyfile(src_txt, dst_txt)
self.updadePathXml(dst_xml, path_img)
def getFolderName(self):
path_out = self.path_out
out = path_out.split('/')
out_name = out[len(out)-1]
return out_name
def defineFolds(self):
'''
separa as imagens em dobras para treinamento e teste
'''
percentage_test = 0
if self.number_fold == 5:
percentage_test = .2
elif self.number_fold == 10:
percentage_test = .1
else:
print('Informe o número de dobras.')
cv = ShuffleSplit(n_splits=self.number_fold, test_size=percentage_test, random_state=0)
images_dict = self.read_dataset()
for class_, images in images_dict.items():
cv.get_n_splits(images)
number_fold = 1
validation_index = None
img_train, img_validation, img_test = None, None, None
for train_index, test_index in cv.split(images):
directory_train = self.path_out + '/' + self.getFolderName() + str(number_fold) + '/' + 'train/' + class_ + '/'
directory_test = self.path_out + '/' + self.getFolderName() + str(number_fold) + '/' + 'test/' + class_ + '/'
self.createDirectory(directory_train)
self.createDirectory(directory_test)
if self.validation:
directory_validation = self.path_out + '/' + self.getFolderName() + str(number_fold) + '/' + 'validation/' + class_ + '/'
self.createDirectory(directory_validation)
percentage_validation = len(images) * (percentage_test * 100) / 100
validation_index = train_index[0:round(percentage_validation)]
train_index = train_index[round(percentage_validation): len(train_index)]
img_validation = [images[i] for i in validation_index]
for path in img_validation:
img = self.readImage(path)
_, file = os.path.split(path)
path_img = directory_validation + file
cv2.imwrite(path_img ,img)
self.copy_files(path, path_img)
number_fold = number_fold + 1
img_train = [images[i] for i in train_index]
img_test = [images[i] for i in test_index]
for path in img_train:
img = self.readImage(path)
_, file = os.path.split(path)
path_img = directory_train + file
cv2.imwrite(path_img ,img)
self.copy_files(path, path_img)
for path in img_test:
img = self.readImage(path)
_, file = os.path.split(path)
path_img = directory_test + file
cv2.imwrite(path_img ,img)
self.copy_files(path, path_img)
def updadePathXml(self, file, text):
tree = ET.parse(file)
tree.find("path").text = text
tree.write(file)
def sampling(self):
if not os.path.exists(self.get_path_out()):
print('Não existe folds, contruindo folds...')
self.defineFolds()
else:
print('Folds já existem.')
exit() # significa que já foi realizado o augmentation e oversampling
if __name__ == '__main__':
path_in = './data/annotations'
path_out = './data/folds'
number_fold = 5
validation = True
cv = CrossValidation(path_out, path_in, number_fold, validation)
cv.sampling()
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "crossvalidation-augmentation-oversampling-for-object-detection.ipynb",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "_84dKyivs6fC"
},
"source": [
"# Como preparar os dados\n",
"Crie uma pasta para seu conjunto de dados. Neste exemplo usaremos a pasta 'annotated_pests_v3'. A pasta 'annotated_pests_v3' tem duas classes de objetos com imagens anotadas com LabelImg. As pastas devem ser organizadas da seguinte forma:\n",
"```\n",
"├── annotated_pests_v3: pasta raiz do conjunto de dados. \n",
" ├── Edessa_meditabunda: uma classe de objetos\n",
" ├── Edessa_meditabunda1.jpg\n",
" ├── Edessa_meditabunda1.xml\n",
" ├── Euschistus_heros Exemplo: uma classe de objetos\n",
" ├── Euschistus_heros1.jpg\n",
" ├── Euschistus_heros1.xml\n",
"```\n",
"\n",
"Caso seu conjunto de dados não seja separado por classes (pastas), suas imagens anotadas devem ser organizadas da seguinte forma:\n",
"```\n",
"├── annotated_pests_v3: pasta raiz do conjunto de dados. \n",
" ├── imagens: pode ser qualquer nomenclatura\n",
" ├── Edessa_meditabunda1.jpg\n",
" ├── Edessa_meditabunda1.xml\n",
" ├── Euschistus_heros1.jpg\n",
" ├── Euschistus_heros1.xml\n",
"```\n",
"Compacte seu conjunto de dados no formato zip e faça o upload em uma pasta no seu Google Drive. No exemplo vamos fazer o upload na pasta Meu Drive/datasets. Veja a organização abaixo.\n",
"```\n",
"├── Meu Drive \n",
" ├── datasets\n",
" ├── annotated_pests_v3.zip\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1ppRsoKdKZgA"
},
"source": [
"# Construindo as amostras"
]
},
{
"cell_type": "code",
"metadata": {
"id": "on-_sjp7WmrL"
},
"source": [
"from google.colab import drive\n",
"drive.mount('/content/gdrive', force_remount=True)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "tRJOq6oGtyec"
},
"source": [
"## Inicializando variáveis\n",
"1. **tipo_amostragem**: Se a amostragem será validação cruzada ou repetições. Exemplo: '**cross**' para validação cruzada, '**repetition**' para repetições. Obs: quando usado 'repetition' sempre serão criadas 10 repetições.\n",
"2. **percentual_aumento_dados**: Se haverá aumento de dados. Se o valor é 0, não haverá aumento de dados. Se o valor é > 0, haverá aumento de dados. Valores válidos para aumento de dados: .1, .2, ... .9,1. (de 10 a 100%). Observação: aumento de dados somente é realizado nos conjuntos de treinamento e validação (se houver).\n",
"3. **number_fold_or_image_train**: Caso '**tipo_amostragem**' seja 'repetition' você deve informar o número de imagens que será usado no treinamento em cada repetição. Exemplo: 30. Dessa forma, serão usadas 30 imagens para treinamento em cada repetição e 70 imagens para testes. O 'number_fold_or_image_train' deve ser <=30. Observação: quando usado o 'tipo_amostragem' setado como 'repetition' o número de imagens para testes sempre será 70. Esse tipo de amostragem foi usado neste artigo para classificação de imagens: https://ieeexplore.ieee.org/document/9170587 . Caso 'tipo_amostragem' seja 'cross' você deve informar o número de dobras. Exemplo: 5 ou 10. \n",
"4. **tem_validacao**: valores válidos 'True' ou 'False'. Se 'True' as repetições ou as dobras serão criadas com dados para validação. Os dados para validação são obtidos do conjunto de treinamento de maneira aleatória. Atualmente, somente 10% dos dados do treinamento são usados para validação. Caso 'tem_validacao' seja 'False' não são criados dados para valiadação. \n",
"5. **tem_oversampling**: Informe se haverá oversampling: True ou False. Observação: oversampling somente é aplicado nos conjuntos de treinamento e validação (se houver).\n",
"6. **dataset_name**: nome do conjunto de dados. Neste exemplo estamos usando 'annotated_pests_v3'.\n",
"7. **path_dataset**: pasta existente no Drive onde está o conjunto de dados compactado (.zip). Neste exemplo estamos usando '/content/gdrive/MyDrive/datasets'."
]
},
{
"cell_type": "code",
"metadata": {
"id": "kBAwfohwNtVx"
},
"source": [
"tipo_amostragem = 'cross' # repetition ou cross\n",
"percentual_aumento_dados = .1 # de .1 a 1\n",
"# se tipo_amostragem = 'cross', então deve ser 5 ou 10.\n",
"# se tipo_amostragem = 'repetition', normalmente usa-se 30, 25, .., 10, 5 images para treinamento. O conjunto de teste é fixado em 70 imagens\n",
"number_fold_or_image_train = 5 \n",
"tem_validacao = True \n",
"tem_oversampling = True\n",
"\n",
"dataset_name = 'annotated_pests_v3'\n",
"path_dataset='/content/gdrive/MyDrive/datasets' # pasta existente no Drive\n"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "MF6v8k0VLaCE"
},
"source": [
"## Clonando o projeto crossvalidation-repetition"
]
},
{
"cell_type": "code",
"metadata": {
"id": "SbI5FgJ2W00T"
},
"source": [
"%cd /content\n",
"!git clone http://git.inovisao.ucdb.br/inovisao/crossvalidation-repetition-object-detection.git"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "Z5QlNd_lLkwn"
},
"source": [
"## Baixando o conjunto de dados"
]
},
{