Source code for pytwovision.datasets_loader.yolov3_dataset_generator

import numpy as np
import cv2 as cv
import os
import random
import tensorflow as tf

from pytwovision.utils.label_utils import read_class_names
from pytwovision.image_process.frame_decorator import Frame
from pytwovision.image_process.resize_with_bbox import ResizeWithBBox
from pytwovision.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