From 67e7a621dc09613de97c02e7831b3e06d161d615 Mon Sep 17 00:00:00 2001 From: Vu Gia Truong Date: Sun, 24 Dec 2017 12:07:46 +0900 Subject: [PATCH 1/8] Change output image path : uuid4.ext -> filename_uuid4.ext --- Augmentor/Pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Augmentor/Pipeline.py b/Augmentor/Pipeline.py index 1c314f4..28c6dec 100644 --- a/Augmentor/Pipeline.py +++ b/Augmentor/Pipeline.py @@ -192,7 +192,8 @@ def _execute(self, augmentor_image, save_to_disk=True): image = operation.perform_operation(image) if save_to_disk: - file_name = str(uuid.uuid4()) + "." + self.save_format + filename, ext = os.path.splitext(os.path.basename(augmentor_image.image_path)) + file_name = "{}_{}.{}".format(filename, str(uuid.uuid4()), self.save_format) try: # A strange error is forcing me to do this at the moment, but will fix later properly # TODO: Fix this! From a4a813e150abc1d3bfd9a8afa548071b07f54dfc Mon Sep 17 00:00:00 2001 From: Vu Gia Truong Date: Sun, 24 Dec 2017 15:11:02 +0900 Subject: [PATCH 2/8] Add NumpyPipeline: generate samples images from a list of numpy array and labels --- Augmentor/NumpyPipeline.py | 135 +++++++++++++++++++++++++++++++++++++ Augmentor/__init__.py | 1 + 2 files changed, 136 insertions(+) create mode 100644 Augmentor/NumpyPipeline.py diff --git a/Augmentor/NumpyPipeline.py b/Augmentor/NumpyPipeline.py new file mode 100644 index 0000000..b955c8b --- /dev/null +++ b/Augmentor/NumpyPipeline.py @@ -0,0 +1,135 @@ +# NumpyPipeline.py +# Author: vugia.truong +# Licensed under the terms of the MIT Licence. +""" +The Pipeline module is the user facing API for the Augmentor package. It +contains the :class:`~Augmentor.Pipeline.Pipeline` class which is used to +create pipeline objects, which can be used to build an augmentation pipeline +by adding operations to the pipeline object. + +For a good overview of how to use Augmentor, along with code samples and +example images, can be seen in the :ref:`mainfeatures` section. +""" +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from builtins import * + +from .Operations import * +from .ImageUtilities import scan_directory, scan, AugmentorImage +from .Pipeline import Pipeline + +import os +import sys +import random +import uuid +import warnings +import numbers +import numpy as np + +from tqdm import tqdm +from PIL import Image + + +class NumpyPipeline(Pipeline): + """ + The Pipeline class handles the creation of augmentation pipelines + and the generation of augmented data by applying operations to + this pipeline. + """ + + # Some class variables we use often + _probability_error_text = "The probability argument must be between 0 and 1." + _threshold_error_text = "The value of threshold must be between 0 and 255." + _valid_formats = ["PNG", "BMP", "GIF", "JPEG"] + _legal_filters = ["NEAREST", "BICUBIC", "ANTIALIAS", "BILINEAR"] + + def __init__(self, images=None, labels=None): + """ + """ + + images_count = len(images) + labels_count = len(labels) + + # Check input image depth + for idx, image in enumerate(images): + channel = np.shape(image)[2] + if channel != 3: + sys.stdout.write("Channel of %d sample does not match : %d instead of %d . Remove it " % + (idx, channel, 3)) + images.pop(idx) + labels.pop(idx) + + if images_count != labels_count: + raise Exception("Number of input images and labels does not match : %d vs %d" % (arrays, labels_count)) + + self.images = images + self.labels = labels + self.operations = [] + + def _execute_with_array(self, image): + """ + Private method used to execute a pipeline on array or matrix data. + :param image: The image to pass through the pipeline. + :type image: Array like object. + :return: The augmented image. + """ + + pil_image = Image.fromarray(image) + + for operation in self.operations: + r = round(random.uniform(0, 1), 1) + if r <= operation.probability: + pil_image = operation.perform_operation(pil_image) + + numpy_array = np.asarray(pil_image) + + return numpy_array + + def sample(self, n): + """ + Generate :attr:`n` number of samples from the current pipeline. + + This function samples from the pipeline, using the original images + defined during instantiation. All images generated by the pipeline + are by default stored in an ``output`` directory, relative to the + path defined during the pipeline's instantiation. + + :param n: The number of new samples to produce. + :type n: Integer + :return: arrays: rendered images + :return: labels: list of image's label + """ + if len(self.operations) == 0: + raise IndexError("There are no operations associated with this pipeline.") + + labels_count = len(self.labels) + samples_total = n * labels_count + progress_bar = tqdm(total=samples_total, desc="Executing Pipeline", unit=' Samples', leave=False) + + image_samples_all = [] + label_samples_all = [] + + for idx, image in enumerate(self.images): + sample_count = 0 + + width, height, depth = np.shape(image) + image_samples = np.zeros((width, height, depth, n), dtype=np.uint8) + while sample_count < n: + image_samples[:, :, :, sample_count] = self._execute_with_array(image) + sample_count += 1 + progress = idx * labels_count + sample_count + progress_bar.set_description("Processing %d in total %d" % (progress, samples_total)) + progress_bar.update(1) + + + image_samples_all.append(image_samples) + label_samples_all = self.labels[idx] * n + + + progress_bar.close() + return image_samples_all, label_samples_all + + + + diff --git a/Augmentor/__init__.py b/Augmentor/__init__.py index 837400a..47f4c54 100644 --- a/Augmentor/__init__.py +++ b/Augmentor/__init__.py @@ -12,6 +12,7 @@ """ from .Pipeline import Pipeline +from .NumpyPipeline import NumpyPipeline __author__ = """Marcus D. Bloice""" __email__ = 'marcus.bloice@medunigraz.at' From 763d375509819b9f65d2bc74bf22b3a16c9294c3 Mon Sep 17 00:00:00 2001 From: Vu Gia Truong Date: Sun, 24 Dec 2017 15:15:01 +0900 Subject: [PATCH 3/8] Add document for numpy pipeline --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 1abb9b8..ccbb063 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,33 @@ p.sample(10000) which will generate 10,000 augmented images based on your specifications. By default these will be written to the disk in a directory named `output` relative to the path specified when initialising the `p` pipeline object above. +### Numpy + +If you want to work with numpy array, you can use the NumpyPipeline: + +```python + +# Read examples image +img1 = cv2.imread("tmp/000.jpg", 1) +img2 = cv2.imread("tmp/000.jpg", 1) +label1 = "1" +label2 = "2" + +# Create list of images +imgs = [img1, img2] +labels = [label1, label2] + +# Create numpy pipline +np_p = Augmentor.NumpyPipeline(imgs, labels) + +# Add operations +np_p.flip_top_bottom(probability=0.5) + +# generate 10 samples +imgs_, labels_ = np_p.sample(10) + +``` + ### Keras and PyTorch If you do not wish to save to disk, you can use a generator (in this case with Keras): From ddeecbef0f051f5de64095bd0b59ffc0329eb58b Mon Sep 17 00:00:00 2001 From: Vu Gia Truong Date: Sun, 24 Dec 2017 15:23:51 +0900 Subject: [PATCH 4/8] Make NumpyPipeLine cleaner --- Augmentor/NumpyPipeline.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/Augmentor/NumpyPipeline.py b/Augmentor/NumpyPipeline.py index b955c8b..3635a30 100644 --- a/Augmentor/NumpyPipeline.py +++ b/Augmentor/NumpyPipeline.py @@ -33,7 +33,7 @@ class NumpyPipeline(Pipeline): """ - The Pipeline class handles the creation of augmentation pipelines + The NumpyPipeline class handles the creation of augmentation pipelines and the generation of augmented data by applying operations to this pipeline. """ @@ -46,6 +46,9 @@ class NumpyPipeline(Pipeline): def __init__(self, images=None, labels=None): """ + Init NumpyPipeline + :param images: List of numpy array of image + :param labels: List of correspoding label of image """ images_count = len(images) @@ -67,38 +70,21 @@ def __init__(self, images=None, labels=None): self.labels = labels self.operations = [] - def _execute_with_array(self, image): - """ - Private method used to execute a pipeline on array or matrix data. - :param image: The image to pass through the pipeline. - :type image: Array like object. - :return: The augmented image. - """ - - pil_image = Image.fromarray(image) - - for operation in self.operations: - r = round(random.uniform(0, 1), 1) - if r <= operation.probability: - pil_image = operation.perform_operation(pil_image) - - numpy_array = np.asarray(pil_image) - - return numpy_array - def sample(self, n): """ Generate :attr:`n` number of samples from the current pipeline. - This function samples from the pipeline, using the original images - defined during instantiation. All images generated by the pipeline - are by default stored in an ``output`` directory, relative to the - path defined during the pipeline's instantiation. + This function generate samples from the NumpyPipeline, + using a list of image (numpy array) and a corresponding list of label which + were defined during instantiation. + For each image with size (w, h, d) a new (w, h, d, n) numpy array is generated. :param n: The number of new samples to produce. :type n: Integer - :return: arrays: rendered images - :return: labels: list of image's label + :return: image_samples_all: rendered images + :type n: List + :return: label_samples_all: list of image's label + :type n: List """ if len(self.operations) == 0: raise IndexError("There are no operations associated with this pipeline.") From f0ec3f300b49310dcecf60a190ed6bf9b01014a7 Mon Sep 17 00:00:00 2001 From: "vugia.truong" Date: Thu, 4 Jan 2018 18:16:54 +0900 Subject: [PATCH 5/8] Fix bug: number of input image is wrong --- Augmentor/NumpyPipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Augmentor/NumpyPipeline.py b/Augmentor/NumpyPipeline.py index 3635a30..9fbc772 100644 --- a/Augmentor/NumpyPipeline.py +++ b/Augmentor/NumpyPipeline.py @@ -64,7 +64,7 @@ def __init__(self, images=None, labels=None): labels.pop(idx) if images_count != labels_count: - raise Exception("Number of input images and labels does not match : %d vs %d" % (arrays, labels_count)) + raise Exception("Number of input images and labels does not match : %d vs %d" % (images_count, labels_count)) self.images = images self.labels = labels From 6188efb58b4a514b4e021e1a5ea60fb9d1752d55 Mon Sep 17 00:00:00 2001 From: "vugia.truong" Date: Thu, 4 Jan 2018 18:39:20 +0900 Subject: [PATCH 6/8] Fix bug: numpy pipeline return a list of 4d image --- Augmentor/NumpyPipeline.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Augmentor/NumpyPipeline.py b/Augmentor/NumpyPipeline.py index 9fbc772..6197a4a 100644 --- a/Augmentor/NumpyPipeline.py +++ b/Augmentor/NumpyPipeline.py @@ -77,7 +77,7 @@ def sample(self, n): This function generate samples from the NumpyPipeline, using a list of image (numpy array) and a corresponding list of label which were defined during instantiation. - For each image with size (w, h, d) a new (w, h, d, n) numpy array is generated. + For each image with size (w, h, d) a new n*(w, h, d) image is generated. :param n: The number of new samples to produce. :type n: Integer @@ -102,16 +102,14 @@ def sample(self, n): width, height, depth = np.shape(image) image_samples = np.zeros((width, height, depth, n), dtype=np.uint8) while sample_count < n: - image_samples[:, :, :, sample_count] = self._execute_with_array(image) + image_sample = self._execute_with_array(image) sample_count += 1 progress = idx * labels_count + sample_count progress_bar.set_description("Processing %d in total %d" % (progress, samples_total)) progress_bar.update(1) - - image_samples_all.append(image_samples) - label_samples_all = self.labels[idx] * n - + image_samples_all.append(image_sample) + label_samples_all.append(self.labels[idx]) progress_bar.close() return image_samples_all, label_samples_all From c92e5b381d87a0a5d512772ad8267e6961129c4e Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 11 Dec 2019 15:47:47 +0900 Subject: [PATCH 7/8] Add missing Numpy Pipeline --- Augmentor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Augmentor/__init__.py b/Augmentor/__init__.py index f8a2b76..f5774d0 100644 --- a/Augmentor/__init__.py +++ b/Augmentor/__init__.py @@ -19,4 +19,4 @@ __email__ = 'marcus.bloice@medunigraz.at' __version__ = '0.2.7' -__all__ = ['Pipeline', 'DataFramePipeline', 'DataPipeline'] +__all__ = ['Pipeline', 'DataFramePipeline', 'DataPipeline', 'NumpyPipeline'] From a8a1754f72e105992abfb6cf4e6ed0cb9c5c3876 Mon Sep 17 00:00:00 2001 From: gachiemchiep Date: Tue, 24 Dec 2019 15:12:17 +0900 Subject: [PATCH 8/8] Add linear motion noise --- Augmentor/Operations.py | 134 +++++++++++++++++++++++++++++++++++++++- Augmentor/Pipeline.py | 15 +++++ 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/Augmentor/Operations.py b/Augmentor/Operations.py index 86fa567..6cc44bb 100644 --- a/Augmentor/Operations.py +++ b/Augmentor/Operations.py @@ -29,12 +29,12 @@ from math import floor, ceil import numpy as np -# from skimage import img_as_ubyte -# from skimage import transform import os import random import warnings +import cv2 +from skimage.draw import line # Python 2-3 compatibility - not currently needed. # try: @@ -735,6 +735,135 @@ def do(image): return augmented_images +class LinearMotion(Operation): + """ + This class is used to perform linear motion on images. + The algorithm is heavily based on https://github.com/lospooky/pyblur/tree/master/pyblur + """ + + def __init__(self, probability, size, angle, linetype): + """ + :param probability: Controls the probability that the operation is + performed when it is invoked in the pipeline. + :param size: size of linear motion kernel + :param angle: angle of linear motion + :param linetype: type of linear motion line + + """ + Operation.__init__(self, probability) + self.size = size + self.angle = angle + self.line_dict = self.gen_motion_lines() + self.kernel = self.get_kernel(size, angle, linetype) + + def get_kernel(self, size, angle, linetype): + kernel_width = size + kernel_center = int(math.floor(size / 2)) + angle = self.get_sanitize_angle_value(kernel_center, angle) + kernel = np.zeros((kernel_width, kernel_width), dtype=np.float32) + line_anchors = self.line_dict[size][angle] + if (linetype == 'right'): + line_anchors[0] = kernel_center + line_anchors[1] = kernel_center + if (linetype == 'left'): + line_anchors[2] = kernel_center + line_anchors[3] = kernel_center + rr, cc = line(line_anchors[0], line_anchors[1], line_anchors[2], line_anchors[3]) + kernel[rr, cc] = 1 + normalization_factor = np.count_nonzero(kernel) + kernel = kernel / normalization_factor + return kernel + + def get_nearest_value(self, theta, valid_angles): + idx = (np.abs(valid_angles - theta)).argmin() + return valid_angles[idx] + + def get_sanitize_angle_value(self, kernel_center, angle): + num_distinct_lines = kernel_center * 4 + angle = math.fmod(angle, 180.0) + valid_line_angles = np.linspace(0, 180, num_distinct_lines, endpoint=False) + angle = self.get_nearest_value(angle, valid_line_angles) + return angle + + def gen_motion_lines(self): + + ret = {} + # 3x3 lines + lines = {} + lines[0] = [1, 0, 1, 2] + lines[45] = [2, 0, 0, 2] + lines[90] = [0, 1, 2, 1] + lines[135] = [0, 0, 2, 2] + ret[3] = lines + + # 5x5 lines + lines = {} + lines[0] = [2, 0, 2, 4] + lines[22.5] = [3, 0, 1, 4] + lines[45] = [0, 4, 4, 0] + lines[67.5] = [0, 3, 4, 1] + lines[90] = [0, 2, 4, 2] + lines[112.5] = [0, 1, 4, 3] + lines[135] = [0, 0, 4, 4] + lines[157.5] = [1, 0, 3, 4] + ret[5] = lines + + # 7x7 lines + lines = {} + lines[0] = [3, 0, 3, 6] + lines[15] = [4, 0, 2, 6] + lines[30] = [5, 0, 1, 6] + lines[45] = [6, 0, 0, 6] + lines[60] = [6, 1, 0, 5] + lines[75] = [6, 2, 0, 4] + lines[90] = [0, 3, 6, 3] + lines[105] = [0, 2, 6, 4] + lines[120] = [0, 1, 6, 5] + lines[135] = [0, 0, 6, 6] + lines[150] = [1, 0, 5, 6] + lines[165] = [2, 0, 4, 6] + ret[7] = lines + + # 9x9 lines + lines = {} + lines[0] = [4, 0, 4, 8] + lines[11.25] = [5, 0, 3, 8] + lines[22.5] = [6, 0, 2, 8] + lines[33.75] = [7, 0, 1, 8] + lines[45] = [8, 0, 0, 8] + lines[56.25] = [8, 1, 0, 7] + lines[67.5] = [8, 2, 0, 6] + lines[78.75] = [8, 3, 0, 5] + lines[90] = [8, 4, 0, 4] + lines[101.25] = [0, 3, 8, 5] + lines[112.5] = [0, 2, 8, 6] + lines[123.75] = [0, 1, 8, 7] + lines[135] = [0, 0, 8, 8] + lines[146.25] = [1, 0, 7, 8] + lines[157.5] = [2, 0, 6, 8] + lines[168.75] = [3, 0, 5, 8] + ret[9] = lines + + return ret + + def perform_operation(self, images): + """ + """ + + def do(image): + img_np = np.array(image) + img_np_filtered = cv2.filter2D(img_np, -1, self.kernel) + + return Image.fromarray(img_np_filtered) + + augmented_images = [] + + for image in images: + augmented_images.append(do(image)) + + return augmented_images + + class RotateRange(Operation): """ This class is used to perform rotations on images by arbitrary numbers of @@ -1832,7 +1961,6 @@ def __init__(self, probability, hue_shift, saturation_scale, saturation_shift, v self.value_shift = value_shift def perform_operation(self, images): - def do(image): hsv = np.array(image.convert("HSV"), 'float64') hsv /= 255. diff --git a/Augmentor/Pipeline.py b/Augmentor/Pipeline.py index efac96e..ff1f931 100644 --- a/Augmentor/Pipeline.py +++ b/Augmentor/Pipeline.py @@ -1011,6 +1011,21 @@ def flip_top_bottom(self, probability): else: self.add_operation(Flip(probability=probability, top_bottom_left_right="TOP_BOTTOM")) + def linear_motion(self, probability, size, angle, linetype="left"): + """ + Perform linear motion blurring + + :param probability: A value between 0 and 1 representing the + probability that the operation should be performed. + :param size: size of kernel + :param angle: motion angle3 + :param linetype: type of line (left, right or full (both left and right)) + """ + if not 0 < probability <= 1: + raise ValueError(Pipeline._probability_error_text) + else: + self.add_operation(LinearMotion(probability=probability, size=size, angle=angle, linetype=linetype)) + def flip_left_right(self, probability): """ Flip (mirror) the image along its horizontal axis, i.e. from left to