import numpy as np
import cv2 as cv
import os
import random
import tensorflow as tf
from py2vision.utils.label_utils import read_class_names
from py2vision.image_process.frame_decorator import Frame
from py2vision.image_process.resize_with_bbox import ResizeWithBBox
from py2vision.compute.yolov3_calculus import YoloV3Calculus
[docs]class YoloV3DatasetGenerator(object):
"""
A generator compatible with YOLO V3 network.
Args:
annotations_path: a path where the annotations are saved.
class_file_name: a path with a .txt file where the classes are saved.
batch_size: an integer that corresponds with the number of image which we introduce in a network per iteration.
data_augmentation: a boolean that controls data augmentation which change original images in new ways.
input_shape: a tuple with the input images dimensions.
strides: a list with the strides in a yolo model.
anchors: these are the yolo anchors sizes.
anchor_per_scale: an integer with the number of anchor boxes per scale.
max_bbox_per_scale: nan integer with the number of bounding boxes per scale.
images_to_ram: a boolean to control when save images in ram which allow a faster training, but it needs more RAM.
"""
def __init__(self, annotations_path, class_file_name, batch_size=4, data_augmentation=True,
input_shape=(416, 416, 3), strides=[8, 16, 32],
anchors=[[[10, 13], [16, 30], [33, 23]],
[[30, 61], [62, 45], [59, 119]],
[[116, 90], [156, 198], [373, 326]]],
anchor_per_scale=3, max_bbox_per_scale=100, images_to_ram=True):
self.annot_path = annotations_path
self.input_sizes = input_shape[0]
self.batch_size = batch_size
self.data_aug = data_augmentation
#self.train_input_sizes = TRAIN_INPUT_SIZE
self.strides = np.array(strides)
self.classes = read_class_names(class_file_name)
self.num_classes = len(self.classes)
self.anchors = (np.array(anchors).T/self.strides).T
self.anchor_per_scale = anchor_per_scale
self.max_bbox_per_scale = max_bbox_per_scale
self.images_to_ram = images_to_ram
self.annotations = self.load_annotations()
self.num_samples = len(self.annotations)
self.num_batchs = int(np.ceil(self.num_samples / self.batch_size))
self.batch_count = 0
[docs] def load_annotations(self):
"""Returns annotations in array shape"""
final_annotations = []
with open(self.annot_path, 'r') as f:
txt = f.readlines()
annotations = [line.strip() for line in txt if len(line.strip().split()[1:]) != 0]
np.random.shuffle(annotations)
for annotation in annotations:
# fully parse annotations
line = annotation.split()
image_path, index = "", 1
for i, one_line in enumerate(line):
if not one_line.replace(",","").isnumeric():
if image_path != "": image_path += " "
image_path += one_line
else:
index = i
break
if not os.path.exists(image_path):
raise KeyError("%s does not exist ... " %image_path)
if self.images_to_ram:
image = cv.imread(image_path)
else:
image = ''
final_annotations.append([image_path, line[index:], image])
return final_annotations
def __iter__(self):
"""Returns its own instance"""
return self
[docs] def delete_bad_annotation(self, bad_annotation):
"""Delete an annotation from annotations file .txt.
Args:
bad_annotation: a string with the path of an image
"""
print(f'Deleting {bad_annotation} annotation line')
bad_image_name = bad_annotation[0].split('/')[-1] # can be used to delete bad image
# remove bad annotation line from annotation file
with open(self.annot_path, "r+") as f:
d = f.readlines()
f.seek(0)
for i in d:
if bad_image_name not in i:
f.write(i)
f.truncate()
def __next__(self):
"""This method will be executed when you iterate this class
Returns:
a batch of images when their bounding boxes
"""
with tf.device('/cpu:0'):
self.train_input_size = random.choice([self.input_sizes])
self.train_output_sizes = self.train_input_size // self.strides
batch_image = np.zeros((self.batch_size, self.train_input_size, self.train_input_size, 3), dtype=np.float32)
batch_label_sbbox = np.zeros((self.batch_size, self.train_output_sizes[0], self.train_output_sizes[0],
self.anchor_per_scale, 5 + self.num_classes), dtype=np.float32)
batch_label_mbbox = np.zeros((self.batch_size, self.train_output_sizes[1], self.train_output_sizes[1],
self.anchor_per_scale, 5 + self.num_classes), dtype=np.float32)
batch_label_lbbox = np.zeros((self.batch_size, self.train_output_sizes[2], self.train_output_sizes[2],
self.anchor_per_scale, 5 + self.num_classes), dtype=np.float32)
batch_sbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4), dtype=np.float32)
batch_mbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4), dtype=np.float32)
batch_lbboxes = np.zeros((self.batch_size, self.max_bbox_per_scale, 4), dtype=np.float32)
exceptions = False
num = 0
if self.batch_count < self.num_batchs:
while num < self.batch_size:
index = self.batch_count * self.batch_size + num
if index >= self.num_samples: index -= self.num_samples
annotation = self.annotations[index]
image, bboxes = self.parse_annotation(annotation)
try:
label_sbbox, label_mbbox, label_lbbox, sbboxes, mbboxes, lbboxes = self.preprocess_true_boxes(bboxes)
except IndexError:
exceptions = True
self.delete_bad_annotation(annotation)
print("IndexError, something wrong with", annotation[0], "removed this line from annotation file")
batch_image[num, :, :, :] = image
batch_label_sbbox[num, :, :, :, :] = label_sbbox
batch_label_mbbox[num, :, :, :, :] = label_mbbox
batch_label_lbbox[num, :, :, :, :] = label_lbbox
batch_sbboxes[num, :, :] = sbboxes
batch_mbboxes[num, :, :] = mbboxes
batch_lbboxes[num, :, :] = lbboxes
num += 1
if exceptions:
print('\n')
raise Exception("There were problems with dataset, I fixed them, now restart the training process.")
self.batch_count += 1
batch_smaller_target = batch_label_sbbox, batch_sbboxes
batch_medium_target = batch_label_mbbox, batch_mbboxes
batch_larger_target = batch_label_lbbox, batch_lbboxes
return batch_image, (batch_smaller_target, batch_medium_target, batch_larger_target)
else:
self.batch_count = 0
np.random.shuffle(self.annotations)
raise StopIteration
[docs] def random_horizontal_flip(self, image, bboxes):
"""There is a probability of 0.5 to make an horizontal flip
Args:
image: it can be an image or a batch of images.
bboxes: this are corresponding bounding boxes.
Returns
images and their bounding boxes.
"""
if random.random() < 0.5:
_, w, _ = image.shape
image = image[:, ::-1, :]
bboxes[:, [0,2]] = w - bboxes[:, [2,0]]
return image, bboxes
[docs] def random_crop(self, image, bboxes):
"""There is a probability of 0.5 to make a crop.
Args:
image: it can be an image or a batch of images.
bboxes: this are corresponding bounding boxes.
Returns
images and their bounding boxes.
"""
if random.random() < 0.5:
h, w, _ = image.shape
max_bbox = np.concatenate([np.min(bboxes[:, 0:2], axis=0), np.max(bboxes[:, 2:4], axis=0)], axis=-1)
max_l_trans = max_bbox[0]
max_u_trans = max_bbox[1]
max_r_trans = w - max_bbox[2]
max_d_trans = h - max_bbox[3]
crop_xmin = max(0, int(max_bbox[0] - random.uniform(0, max_l_trans)))
crop_ymin = max(0, int(max_bbox[1] - random.uniform(0, max_u_trans)))
crop_xmax = max(w, int(max_bbox[2] + random.uniform(0, max_r_trans)))
crop_ymax = max(h, int(max_bbox[3] + random.uniform(0, max_d_trans)))
image = image[crop_ymin : crop_ymax, crop_xmin : crop_xmax]
bboxes[:, [0, 2]] = bboxes[:, [0, 2]] - crop_xmin
bboxes[:, [1, 3]] = bboxes[:, [1, 3]] - crop_ymin
return image, bboxes
[docs] def random_translate(self, image, bboxes):
"""There is a probability of 0.5 to shift in x and y images.
Args:
image: it can be an image or a batch of images.
bboxes: this are corresponding bounding boxes.
Returns
images and their bounding boxes.
"""
if random.random() < 0.5:
h, w, _ = image.shape
max_bbox = np.concatenate([np.min(bboxes[:, 0:2], axis=0), np.max(bboxes[:, 2:4], axis=0)], axis=-1)
max_l_trans = max_bbox[0]
max_u_trans = max_bbox[1]
max_r_trans = w - max_bbox[2]
max_d_trans = h - max_bbox[3]
tx = random.uniform(-(max_l_trans - 1), (max_r_trans - 1))
ty = random.uniform(-(max_u_trans - 1), (max_d_trans - 1))
M = np.array([[1, 0, tx], [0, 1, ty]])
image = cv.warpAffine(image, M, (w, h))
bboxes[:, [0, 2]] = bboxes[:, [0, 2]] + tx
bboxes[:, [1, 3]] = bboxes[:, [1, 3]] + ty
return image, bboxes
[docs] def parse_annotation(self, annotation, mAP=False):
"""Convert an annotation in an image with their bounding boxes.
Args:
annotation: an annotation line (path bounding box 1, class bounding box 2, class, ...)
mAP: when it's true this method won't resize images and bounding boxes, otherwise it will do.
Returns
images and their bounding boxes.
"""
if self.images_to_ram:
image_path = annotation[0]
image = annotation[2]
else:
image_path = annotation[0]
image = cv.imread(image_path)
bboxes = np.array([list(map(int, box.split(','))) for box in annotation[1]])
if self.data_aug:
image, bboxes = self.random_horizontal_flip(np.copy(image), np.copy(bboxes))
image, bboxes = self.random_crop(np.copy(image), np.copy(bboxes))
image, bboxes = self.random_translate(np.copy(image), np.copy(bboxes))
if mAP == True:
return image, bboxes
frame = Frame(np.copy(image))
image, bboxes = ResizeWithBBox(frame).apply([self.input_sizes, self.input_sizes], np.copy(bboxes))
return image, bboxes
[docs] def preprocess_true_boxes(self, bboxes):
"""To prepare labels (true_boxes).
Args:
bboxes: a bounding boxes array.
Returns:
Ground true bounding boxes
"""
label = [np.zeros((self.train_output_sizes[i], self.train_output_sizes[i], self.anchor_per_scale,
5 + self.num_classes)) for i in range(3)]
bboxes_xywh = [np.zeros((self.max_bbox_per_scale, 4)) for _ in range(3)]
bbox_count = np.zeros((3,))
compute = YoloV3Calculus()
for bbox in bboxes:
bbox_coor = bbox[:4]
bbox_class_ind = bbox[4]
onehot = np.zeros(self.num_classes, dtype=np.float)
onehot[bbox_class_ind] = 1.0
uniform_distribution = np.full(self.num_classes, 1.0 / self.num_classes)
deta = 0.01
smooth_onehot = onehot * (1 - deta) + deta * uniform_distribution
bbox_xywh = np.concatenate([(bbox_coor[2:] + bbox_coor[:2]) * 0.5, bbox_coor[2:] - bbox_coor[:2]], axis=-1)
bbox_xywh_scaled = 1.0 * bbox_xywh[np.newaxis, :] / self.strides[:, np.newaxis]
iou = []
exist_positive = False
for i in range(3):
anchors_xywh = np.zeros((self.anchor_per_scale, 4))
anchors_xywh[:, 0:2] = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32) + 0.5
anchors_xywh[:, 2:4] = self.anchors[i]
iou_scale = compute.bbox_iou(bbox_xywh_scaled[i][np.newaxis, :], anchors_xywh)
iou.append(iou_scale)
iou_mask = iou_scale > 0.3
if np.any(iou_mask):
xind, yind = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32)
label[i][yind, xind, iou_mask, :] = 0
label[i][yind, xind, iou_mask, 0:4] = bbox_xywh
label[i][yind, xind, iou_mask, 4:5] = 1.0
label[i][yind, xind, iou_mask, 5:] = smooth_onehot
bbox_ind = int(bbox_count[i] % self.max_bbox_per_scale)
bboxes_xywh[i][bbox_ind, :4] = bbox_xywh
bbox_count[i] += 1
exist_positive = True
if not exist_positive:
best_anchor_ind = np.argmax(np.array(iou).reshape(-1), axis=-1)
best_detect = int(best_anchor_ind / self.anchor_per_scale)
best_anchor = int(best_anchor_ind % self.anchor_per_scale)
xind, yind = np.floor(bbox_xywh_scaled[best_detect, 0:2]).astype(np.int32)
label[best_detect][yind, xind, best_anchor, :] = 0
label[best_detect][yind, xind, best_anchor, 0:4] = bbox_xywh
label[best_detect][yind, xind, best_anchor, 4:5] = 1.0
label[best_detect][yind, xind, best_anchor, 5:] = smooth_onehot
bbox_ind = int(bbox_count[best_detect] % self.max_bbox_per_scale)
bboxes_xywh[best_detect][bbox_ind, :4] = bbox_xywh
bbox_count[best_detect] += 1
label_sbbox, label_mbbox, label_lbbox = label
sbboxes, mbboxes, lbboxes = bboxes_xywh
return label_sbbox, label_mbbox, label_lbbox, sbboxes, mbboxes, lbboxes
def __len__(self):
"""Get num of batches it will be num_samples / batch_size"""
return self.num_batchs