Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pipeline for numpy #59

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
119 changes: 119 additions & 0 deletions Augmentor/NumpyPipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# NumpyPipeline.py
# Author: vugia.truong <https://github.com/gachiemchiep>
# 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 NumpyPipeline 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):
"""
Init NumpyPipeline
:param images: List of numpy array of image
:param labels: List of correspoding label of image
"""

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" % (images_count, labels_count))

self.images = images
self.labels = labels
self.operations = []

def sample(self, n):
"""
Generate :attr:`n` number of samples from the current pipeline.

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 n*(w, h, d) image is generated.

:param n: The number of new samples to produce.
:type n: Integer
: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.")

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_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_sample)
label_samples_all.append(self.labels[idx])

progress_bar.close()
return image_samples_all, label_samples_all




134 changes: 131 additions & 3 deletions Augmentor/Operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 17 additions & 1 deletion Augmentor/Pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=True):
# save_to_disk = False

if save_to_disk:
file_name = str(uuid.uuid4())
filename, ext = os.path.splitext(os.path.basename(augmentor_image.image_path))
file_name = "{}_{}.{}".format(filename, str(uuid.uuid4()), self.save_format)
try:
for i in range(len(images)):
if i == 0:
Expand Down Expand Up @@ -1010,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
Expand Down
4 changes: 3 additions & 1 deletion Augmentor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

"""

from .Pipeline import Pipeline
from .NumpyPipeline import NumpyPipeline
from .Pipeline import Pipeline, DataFramePipeline, DataPipeline

__author__ = """Marcus D. Bloice"""
__email__ = 'marcus.bloice@medunigraz.at'
__version__ = '0.2.7'

__all__ = ['Pipeline', 'DataFramePipeline', 'DataPipeline']
__all__ = ['Pipeline', 'DataFramePipeline', 'DataPipeline', 'NumpyPipeline']
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ 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 wish to process each image in the pipeline exactly once, use `process()`:

```python
Expand Down