-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathPipeline.py
1936 lines (1608 loc) · 82.4 KB
/
Pipeline.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Pipeline.py
# Author: Marcus D. Bloice <https://github.com/mdbloice> and contributors
# 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 PIL.Image import Image
from builtins import *
from .Operations import *
from .ImageUtilities import scan_directory, scan, scan_dataframe, AugmentorImage
import os
import sys
import random
import uuid
import warnings
import numpy as np
from concurrent.futures import ThreadPoolExecutor
# NOTE:
# https://pypi.org/project/futures/ mentions:
# The ProcessPoolExecutor class has known (unfixable) problems on Python 2 and
# should not be relied on for mission critical work.
from tqdm import tqdm
from PIL import Image
class Pipeline(object):
"""
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, source_directory=None, output_directory="output", save_format=None):
"""
Create a new Pipeline object pointing to a directory containing your
original image dataset.
Create a new Pipeline object, using the :attr:`source_directory`
parameter as a source directory where your original images are
stored. This folder will be scanned, and any valid file files
will be collected and used as the original dataset that should
be augmented. The scan will find any image files with the extensions
JPEG/JPG, PNG, and GIF (case insensitive).
:param source_directory: A directory on your filesystem where your
original images are stored.
:param output_directory: Specifies where augmented images should be
saved to the disk. Default is the directory **output** relative to
the path where the original image set was specified. If it does not
exist it will be created.
:param save_format: The file format to use when saving newly created,
augmented images. Default is JPEG. Legal options are BMP, PNG, and
GIF.
:return: A :class:`Pipeline` object.
"""
# TODO: Allow a single image to be added when initialising.
# Initialise some variables for the Pipeline object.
self.image_counter = 0
self.augmentor_images = []
self.distinct_dimensions = set()
self.distinct_formats = set()
self.save_format = save_format
self.operations = []
self.class_labels = []
self.process_ground_truth_images = False
if source_directory is not None:
self._populate(source_directory=source_directory,
output_directory=output_directory,
ground_truth_directory=None,
ground_truth_output_directory=output_directory)
def __call__(self, augmentor_image):
"""
Function used by the ThreadPoolExecutor to process the pipeline
using multiple threads. Do not call directly.
This function does nothing except call :func:`_execute`, rather
than :func:`_execute` being called directly in :func:`sample`.
This makes it possible for the procedure to be *pickled* and
therefore suitable for multi-threading.
:param augmentor_image: The image to pass through the pipeline.
:return: None
"""
return self._execute(augmentor_image)
def _populate(self, source_directory, output_directory, ground_truth_directory, ground_truth_output_directory):
"""
Private method for populating member variables with AugmentorImage
objects for each of the images found in the source directory
specified by the user. It also populates a number of fields such as
the :attr:`output_directory` member variable, used later when saving
images to disk.
This method is used by :func:`__init__`.
:param source_directory: The directory to scan for images.
:param output_directory: The directory to set for saving files.
Defaults to a directory named output relative to
:attr:`source_directory`.
:param ground_truth_directory: A directory containing ground truth
files for the associated images in the :attr:`source_directory`
directory.
:param ground_truth_output_directory: A path to a directory to store
the output of the operations on the ground truth data set.
:type source_directory: String
:type output_directory: String
:type ground_truth_directory: String
:type ground_truth_output_directory: String
:return: None
"""
# Check if the source directory for the original images to augment exists at all
if not os.path.exists(source_directory):
raise IOError("The source directory you specified does not exist.")
# If a ground truth directory is being specified we will check here if the path exists at all.
if ground_truth_directory:
if not os.path.exists(ground_truth_directory):
raise IOError("The ground truth source directory you specified does not exist.")
# Get absolute path for output
abs_output_directory = os.path.join(source_directory, output_directory)
# Scan the directory that user supplied.
self.augmentor_images, self.class_labels = scan(source_directory, abs_output_directory)
self._check_images(abs_output_directory)
def _populate_image_arrays(self):
"""
Private method. Do not call directly.
When passed image arrays, populate each AugmentorImage
with the array data.
Currently not implemented.
:return: None
"""
warnings.warn("Currently not implemented. Do not call directly.")
return 1
def _check_images(self, abs_output_directory):
"""
Private method. Used to check images as they are added to the
pipeline. Do not call directly.
:param abs_output_directory: the absolute path of the output directory
:return:
"""
# Make output directory/directories
if len(set(self.class_labels)) <= 1:
if not os.path.exists(abs_output_directory):
try:
os.makedirs(abs_output_directory)
except IOError:
print("Insufficient rights to read or write output directory (%s)"
% abs_output_directory)
else:
for class_label in self.class_labels:
if not os.path.exists(os.path.join(abs_output_directory, str(class_label[0]))):
try:
os.makedirs(os.path.join(abs_output_directory, str(class_label[0])))
except IOError:
print("Insufficient rights to read or write output directory (%s)"
% abs_output_directory)
for augmentor_image in self.augmentor_images:
try:
with Image.open(augmentor_image.image_path) as opened_image:
self.distinct_dimensions.add(opened_image.size)
self.distinct_formats.add(opened_image.format)
except IOError as e:
print("There is a problem with image %s in your source directory: %s"
% (augmentor_image.image_path, e.message))
self.augmentor_images.remove(augmentor_image)
sys.stdout.write("Initialised with %s image(s) found.\n" % len(self.augmentor_images))
sys.stdout.write("Output directory set to %s." % abs_output_directory)
def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=True):
"""
Private method. Used to pass an image through the current pipeline,
and return the augmented image.
The returned image can then either be saved to disk or simply passed
back to the user. Currently this is fixed to True, as Augmentor
has only been implemented to save to disk at present.
:param augmentor_image: The image to pass through the pipeline.
:param save_to_disk: Whether to save the image to disk. Currently
fixed to true.
:type augmentor_image: :class:`ImageUtilities.AugmentorImage`
:type save_to_disk: Boolean
:return: The augmented image.
"""
images = []
if augmentor_image.image_path is not None:
images.append(Image.open(augmentor_image.image_path))
# What if they are array data?
if augmentor_image.pil_images is not None:
images.append(augmentor_image.pil_images)
if augmentor_image.ground_truth is not None:
if isinstance(augmentor_image.ground_truth, list):
for image in augmentor_image.ground_truth:
images.append(Image.open(image))
else:
images.append(Image.open(augmentor_image.ground_truth))
for operation in self.operations:
r = round(random.uniform(0, 1), 1)
if r <= operation.probability:
images = operation.perform_operation(images)
# TEMP FOR TESTING
# save_to_disk = False
if save_to_disk:
file_name = str(uuid.uuid4())
try:
for i in range(len(images)):
if i == 0:
save_name = augmentor_image.class_label \
+ "_original_" \
+ os.path.basename(augmentor_image.image_path) \
+ "_" \
+ file_name \
+ "." \
+ (self.save_format if self.save_format else augmentor_image.file_format)
images[i].save(os.path.join(augmentor_image.output_directory, save_name))
else:
save_name = "_groundtruth_(" \
+ str(i) \
+ ")_" \
+ augmentor_image.class_label \
+ "_" \
+ os.path.basename(augmentor_image.image_path) \
+ "_" \
+ file_name \
+ "." \
+ (self.save_format if self.save_format else augmentor_image.file_format)
images[i].save(os.path.join(augmentor_image.output_directory, save_name))
except IOError as e:
print("Error writing %s, %s. Change save_format to PNG?" % (file_name, e.message))
print("You can change the save format using the set_save_format(save_format) function.")
print("By passing save_format=\"auto\", Augmentor can save in the correct format automatically.")
# TODO: Fix this really strange behaviour.
# As a workaround, we can pass the same back and basically
# ignore the multi_threaded parameter completely for now.
# if multi_threaded:
# return os.path.basename(augmentor_image.image_path)
# else:
# return images[0] # Here we return only the first image for the generators.
# return images[0] # old method.
return images[0]
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[0])
return numpy_array
def set_save_format(self, save_format):
"""
Set the save format for the pipeline. Pass the value
:attr:`save_format="auto"` to allow Augmentor to choose
the correct save format based on each individual image's
file extension.
If :attr:`save_format` is set to, for example,
:attr:`save_format="JPEG"` or :attr:`save_format="JPG"`,
Augmentor will attempt to save the files using the
JPEG format, which may result in errors if the file cannot
be saved in this format, such as trying to save PNG images
with an alpha channel as JPEG.
:param save_format: The save format to save the images
when writing to disk.
:return: None
"""
if save_format == "auto":
self.save_format = None
else:
self.save_format = save_format
def sample(self, n, multi_threaded=True):
"""
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.
By default, Augmentor will use multi-threading to increase the speed
of processing the images. However, this may slow down some
operations if the images are very small. Set :attr:`multi_threaded`
to ``False`` if slowdown is experienced.
:param n: The number of new samples to produce.
:type n: Integer
:param multi_threaded: Whether to use multi-threading to process the
images. Defaults to ``True``.
:type multi_threaded: Boolean
:return: None
"""
if len(self.augmentor_images) == 0:
raise IndexError("There are no images in the pipeline. "
"Add a directory using add_directory(), "
"pointing it to a directory containing images.")
if len(self.operations) == 0:
raise IndexError("There are no operations associated with this pipeline.")
if n == 0:
augmentor_images = self.augmentor_images
else:
augmentor_images = [random.choice(self.augmentor_images) for _ in range(n)]
if multi_threaded:
# TODO: Restore the functionality (appearance of progress bar) from the pre-multi-thread code above.
with tqdm(total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples") as progress_bar:
with ThreadPoolExecutor(max_workers=None) as executor:
for result in executor.map(self, augmentor_images):
progress_bar.set_description("Processing %s" % result)
progress_bar.update(1)
else:
with tqdm(total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples") as progress_bar:
for augmentor_image in augmentor_images:
self._execute(augmentor_image)
progress_bar.set_description("Processing %s" % os.path.basename(augmentor_image.image_path))
progress_bar.update(1)
# This does not work as it did in the pre-multi-threading code above for some reason.
# progress_bar.close()
def process(self):
"""
This function is used to process every image in the pipeline
exactly once.
This might be useful for resizing a dataset for example, and
uses multi-threading for fast execution.
It would make sense to set the probability of every operation
in the pipeline to ``1`` when using this function.
:return: None
"""
self.sample(0, multi_threaded=True)
return None
def sample_with_array(self, image_array, save_to_disk=False):
"""
Generate images using a single image in array-like format.
.. seealso::
See :func:`keras_image_generator_without_replacement()`
:param image_array: The image to pass through the pipeline.
:param save_to_disk: Whether to save to disk or not (default).
:return:
"""
a = AugmentorImage(image_path=None, output_directory=None)
a.image_PIL = Image.fromarray(image_array)
return self._execute(a, save_to_disk)
@staticmethod
def categorical_labels(numerical_labels):
"""
Return categorical labels for an array of 0-based numerical labels.
:param numerical_labels: The numerical labels.
:type numerical_labels: Array-like list.
:return: The categorical labels.
"""
# class_labels_np = np.array([x.class_label_int for x in numerical_labels])
class_labels_np = np.array(numerical_labels)
one_hot_encoding = np.zeros((class_labels_np.size, class_labels_np.max() + 1))
one_hot_encoding[np.arange(class_labels_np.size), class_labels_np] = 1
one_hot_encoding = one_hot_encoding.astype(np.uint)
return one_hot_encoding
def image_generator(self):
"""
Deprecated. Use the sample function and return a generator.
:return: A random image passed through the pipeline.
"""
warnings.warn("This function has been deprecated.", DeprecationWarning)
while True:
im_index = random.randint(0, len(self.augmentor_images)-1) # Fix for issue 52.
yield self._execute(self.augmentor_images[im_index], save_to_disk=False)
def generator_threading_tests(self, batch_size):
while True:
return_results = []
augmentor_images = [random.choice(self.augmentor_images) for _ in range(batch_size)]
with ThreadPoolExecutor(max_workers=None) as executor:
for result in executor.map(self, augmentor_images):
return_results.append(result)
yield return_results
def generator_threading_tests_with_matrix_data(self, images, label):
self.augmentor_images = [AugmentorImage(image_path=None, output_directory=None, pil_images=x, label=y)
for x, y in zip(images, label)]
return 1
# TODO: Fix: scaled=True results in an error.
def keras_generator(self, batch_size, scaled=True, scale_function=None, image_data_format="channels_last"):
"""
Returns an image generator that will sample from the current pipeline
indefinitely, as long as it is called.
.. warning::
This function returns images from the current pipeline
**with replacement**.
You must configure the generator to provide data in the same
format that Keras is configured for. You can use the functions
:func:`keras.backend.image_data_format()` and
:func:`keras.backend.set_image_data_format()` to get and set
Keras' image format at runtime.
.. code-block:: python
>>> from keras import backend as K
>>> K.image_data_format()
'channels_first'
>>> K.set_image_data_format('channels_last')
>>> K.image_data_format()
'channels_last'
By default, Augmentor uses ``'channels_last'``.
:param batch_size: The number of images to return per batch.
:type batch_size: Integer
:param scaled: True (default) if pixels are to be converted
to float32 values between 0 and 1, or False if pixels should be
integer values between 0-255.
:type scaled: Boolean
:param image_data_format: Either ``'channels_last'`` (default) or
``'channels_first'``.
:type image_data_format: String
:return: An image generator.
"""
if image_data_format not in ["channels_first", "channels_last"]:
warnings.warn("To work with Keras, must be one of channels_first or channels_last.")
while True:
# Randomly select 25 images for augmentation and yield the
# augmented images.
# X = np.array([])
# y = np.array([])
# The correct thing to do here is to pre-allocate
# batch = np.ndarray((batch_size, 28, 28, 1))
X = []
y = []
for i in range(batch_size):
# Pre-allocate
# batch[i:i+28]
# Select random image, get image array and label
random_image_index = random.randint(0, len(self.augmentor_images)-1)
numpy_array = np.asarray(self._execute(self.augmentor_images[random_image_index], save_to_disk=False))
label = self.augmentor_images[random_image_index].categorical_label
# Reshape
w = numpy_array.shape[0]
h = numpy_array.shape[1]
if np.ndim(numpy_array) == 2:
l = 1
else:
l = np.shape(numpy_array)[2]
if image_data_format == "channels_last":
numpy_array = numpy_array.reshape(w, h, l)
elif image_data_format == "channels_first":
numpy_array = numpy_array.reshape(l, w, h)
X.append(numpy_array)
y.append(label)
X = np.asarray(X)
y = np.asarray(y)
if scaled:
if scale_function is None:
X = X.astype('float32')
X /= 255. # PR #126
else:
X = scale_function(X)
yield (X, y)
def keras_generator_from_array(self, images, labels, batch_size, scaled=True, image_data_format="channels_last"):
"""
Returns an image generator that will sample from the current pipeline
indefinitely, as long as it is called.
.. warning::
This function returns images from :attr:`images`
**with replacement**.
You must configure the generator to provide data in the same
format that Keras is configured for. You can use the functions
:func:`keras.backend.image_data_format()` and
:func:`keras.backend.set_image_data_format()` to get and set
Keras' image format at runtime.
.. code-block:: python
>>> from keras import backend as K
>>> K.image_data_format()
'channels_first'
>>> K.set_image_data_format('channels_last')
>>> K.image_data_format()
'channels_last'
By default, Augmentor uses ``'channels_last'``.
:param images: The images to augment using the current pipeline.
:type images: Array-like matrix. For greyscale images they can be
in the form ``(l, x, y)`` or ``(l, x, y, 1)``, where
:attr:`l` is the number of images, :attr:`x` is the image width
and :attr:`y` is the image height. For RGB/A images, the matrix
should be in the form ``(l, x, y, n)``, where :attr:`n` is the
number of layers, e.g. 3 for RGB or 4 for RGBA and CMYK.
:param labels: The label associated with each image in :attr:`images`.
:type labels: List.
:param batch_size: The number of images to return per batch.
:type batch_size: Integer
:param scaled: True (default) if pixels are to be converted
to float32 values between 0 and 1, or False if pixels should be
integer values between 0-255.
:type scaled: Boolean
:param image_data_format: Either ``'channels_last'`` (default) or
``'channels_first'``. When ``'channels_last'`` is specified the
returned batch is in the form ``(batch_size, x, y, num_channels)``,
while for ``'channels_last'`` the batch is returned in the form
``(batch_size, num_channels, x, y)``.
:param image_data_format: String
:return: An image generator.
"""
# Here, we will expect an matrix in the shape (l, x, y)
# where l is the number of images
# Check if the labels and images align
if len(images) != len(labels):
raise IndexError("The number of images does not match the number of labels.")
while True:
X = []
y = []
for i in range(batch_size):
random_image_index = random.randint(0, len(images)-1)
# Before passing the image we must format it in a shape that
# Pillow can understand, that is either (w, h) for greyscale
# or (w, h, num_channels) for RGB, RGBA, or CMYK images.
# PIL expects greyscale or B&W images in the form (w, h)
# and RGB(A) images images in the form (w, h, n) where n is
# the number of channels, which is 3 or 4.
# However, Keras often works with greyscale/B&W images in the
# form (w, h, 1). We will convert all images to (w, h) if they
# are not RGB, otherwise we will use (w, h, n).
if np.ndim(images) == 3:
l = 1
else:
l = np.shape(images)[-1]
w = images[random_image_index].shape[0]
h = images[random_image_index].shape[1]
if l == 1:
numpy_array = self._execute_with_array(np.reshape(images[random_image_index], (w, h)))
else:
numpy_array = self._execute_with_array(np.reshape(images[random_image_index], (w, h, l)))
if image_data_format == "channels_first":
numpy_array = numpy_array.reshape(l, w, h)
elif image_data_format == "channels_last":
numpy_array = numpy_array.reshape(w, h, l)
X.append(numpy_array)
y.append(labels[random_image_index])
X = np.asarray(X)
y = np.asarray(y)
if scaled:
X = X.astype('float32')
X /= 255. # PR #126
yield(X, y)
def keras_preprocess_func(self):
"""
Returns the pipeline as a function that can be used with Keras ImageDataGenerator.
The image array data fed to the returned function is supposed to have scaled to [0, 1].
It will be once converted to PIL format internally as
`Image.fromarray(np.uint8(255 * image))`.
.. code-block:: python
>>> import Augmentor
>>> import torchvision
>>> p = Augmentor.Pipeline()
>>> p.rotate(probability=0.7, max_left_rotate=10, max_right_rotate=10)
>>> p.zoom(probability=0.5, min_factor=1.1, max_factor=1.5)
>>> from keras.preprocessing.image import ImageDataGenerator
>>> datagen = ImageDataGenerator(
>>> ...
>>> preprocessing_function=p.keras_preprocess_func())
:return: The pipeline as a function.
"""
def _transform_keras_preprocess_func(image):
image = Image.fromarray(np.uint8(255 * image))
for operation in self.operations:
r = random.uniform(0, 1)
if r < operation.probability:
image = operation.perform_operation([image])[0]
#a = AugmentorImage(image_path=None, output_directory=None)
#a.image_PIL =
return image #self._execute(a)
return _transform_keras_preprocess_func
def torch_transform(self):
"""
Returns the pipeline as a function that can be used with torchvision.
.. code-block:: python
>>> import Augmentor
>>> import torchvision
>>> p = Augmentor.Pipeline()
>>> p.rotate(probability=0.7, max_left_rotate=10, max_right_rotate=10)
>>> p.zoom(probability=0.5, min_factor=1.1, max_factor=1.5)
>>> transforms = torchvision.transforms.Compose([
>>> p.torch_transform(),
>>> torchvision.transforms.ToTensor(),
>>> ])
:return: The pipeline as a function.
"""
def _transform(image):
for operation in self.operations:
r = random.uniform(0, 1)
if r < operation.probability:
image = operation.perform_operation([image])[0]
return image
return _transform
def perform_operations(self, image):
augmented_image = image
for operation in self.operations:
r = round(random.uniform(0, 1), 1)
if r <= operation.probability:
augmented_image = operation.perform_operation([augmented_image])[0]
return augmented_image
def add_operation(self, operation):
"""
Add an operation directly to the pipeline. Can be used to add custom
operations to a pipeline.
To add custom operations to a pipeline, subclass from the
Operation abstract base class, overload its methods, and insert the
new object into the pipeline using this method.
.. seealso:: The :class:`.Operation` class.
:param operation: An object of the operation you wish to add to the
pipeline. Will accept custom operations written at run-time.
:type operation: Operation
:return: None
"""
if isinstance(operation, Operation):
self.operations.append(operation)
else:
raise TypeError("Must be of type Operation to be added to the pipeline.")
def remove_operation(self, operation_index=-1):
"""
Remove the operation specified by :attr:`operation_index`, if
supplied, otherwise it will remove the latest operation added to the
pipeline.
.. seealso:: Use the :func:`status` function to find an operation's
index.
:param operation_index: The index of the operation to remove.
:type operation_index: Integer
:return: The removed operation. You can reinsert this at end of the
pipeline using :func:`add_operation` if required.
"""
# Python's own List exceptions can handle erroneous user input.
self.operations.pop(operation_index)
def add_further_directory(self, new_source_directory, new_output_directory="output"):
"""
Add a further directory containing images you wish to scan for augmentation.
:param new_source_directory: The directory to scan for images.
:param new_output_directory: The directory to use for outputted,
augmented images.
:type new_source_directory: String
:type new_output_directory: String
:return: None
"""
if not os.path.exists(new_source_directory):
raise IOError("The path does not appear to exist.")
self._populate(source_directory=new_source_directory,
output_directory=new_output_directory,
ground_truth_directory=None,
ground_truth_output_directory=new_output_directory)
def status(self):
"""
Prints the status of the pipeline to the console. If you want to
remove an operation, use the index shown and the
:func:`remove_operation` method.
.. seealso:: The :func:`remove_operation` function.
.. seealso:: The :func:`add_operation` function.
The status includes the number of operations currently attached to
the pipeline, each operation's parameters, the number of images in the
pipeline, and a summary of the images' properties, such as their
dimensions and formats.
:return: None
"""
# TODO: Return this as a dictionary of some kind and print from the dict if in console
print("Operations: %s" % len(self.operations))
if len(self.operations) != 0:
operation_index = 0
for operation in self.operations:
print("\t%s: %s (" % (operation_index, operation), end="")
for operation_attribute, operation_value in operation.__dict__.items():
print("%s=%s " % (operation_attribute, operation_value), end="")
print(")")
operation_index += 1
print("Images: %s" % len(self.augmentor_images))
# TODO: find a better way that doesn't need to iterate over every image
# TODO: get rid of this label_pair property as nowhere else uses it
# Check if we have any labels before printing label information.
label_count = 0
for image in self.augmentor_images:
if image.label_pair is not None:
label_count += 1
if label_count != 0:
label_pairs = sorted(set([x.label_pair for x in self.augmentor_images]))
print("Classes: %s" % len(label_pairs))
for label_pair in label_pairs:
print ("\tClass index: %s Class label: %s " % (label_pair[0], label_pair[1]))
if len(self.augmentor_images) != 0:
print("Dimensions: %s" % len(self.distinct_dimensions))
for distinct_dimension in self.distinct_dimensions:
print("\tWidth: %s Height: %s" % (distinct_dimension[0], distinct_dimension[1]))
print("Formats: %s" % len(self.distinct_formats))
for distinct_format in self.distinct_formats:
print("\t %s" % distinct_format)
print("\nYou can remove operations using the appropriate index and the remove_operation(index) function.")
@staticmethod
def set_seed(seed):
"""
Set the seed of Python's internal random number generator.
:param seed: The seed to use. Strings or other objects will be hashed.
:type seed: Integer
:return: None
"""
random.seed(seed)
# TODO: Implement
# def subtract_mean(self, probability=1):
# # For implementation example, see bottom of:
# # https://patrykchrabaszcz.github.io/Imagenet32/
# self.add_operation(Mean(probability=probability))
def rotate90(self, probability):
"""
Rotate an image by 90 degrees.
The operation will rotate an image by 90 degrees, and will be
performed with a probability of that specified by the
:attr:`probability` parameter.
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:type probability: Float
:return: None
"""
if not 0 < probability <= 1:
raise ValueError(Pipeline._probability_error_text)
else:
self.add_operation(Rotate(probability=probability, rotation=90))
def rotate180(self, probability):
"""
Rotate an image by 180 degrees.
The operation will rotate an image by 180 degrees, and will be
performed with a probability of that specified by the
:attr:`probability` parameter.
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:type probability: Float
:return: None
"""
if not 0 < probability <= 1:
raise ValueError(Pipeline._probability_error_text)
else:
self.add_operation(Rotate(probability=probability, rotation=180))
def rotate270(self, probability):
"""
Rotate an image by 270 degrees.
The operation will rotate an image by 270 degrees, and will be
performed with a probability of that specified by the
:attr:`probability` parameter.
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:type probability: Float
:return: None
"""
if not 0 < probability <= 1:
raise ValueError(Pipeline._probability_error_text)
else:
self.add_operation(Rotate(probability=probability, rotation=270))
def rotate_random_90(self, probability):
"""
Rotate an image by either 90, 180, or 270 degrees, selected randomly.
This function will rotate by either 90, 180, or 270 degrees. This is
useful to avoid scenarios where images may be rotated back to their
original positions (such as a :func:`rotate90` and a :func:`rotate270`
being performed directly afterwards. The random rotation is chosen
uniformly from 90, 180, or 270 degrees. The probability controls the
chance of the operation being performed at all, and does not affect
the rotation degree.
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:type probability: Float
:return: None
"""
if not 0 < probability <= 1:
raise ValueError(Pipeline._probability_error_text)
else:
self.add_operation(Rotate(probability=probability, rotation=-1))
def rotate(self, probability, max_left_rotation, max_right_rotation):
"""
Rotate an image by an arbitrary amount.
The operation will rotate an image by an random amount, within a range
specified. The parameters :attr:`max_left_rotation` and
:attr:`max_right_rotation` allow you to control this range. If you
wish to rotate the images by an exact number of degrees, set both
:attr:`max_left_rotation` and :attr:`max_right_rotation` to the same
value.
.. note:: This function will rotate **in place**, and crop the largest
possible rectangle from the rotated image.
In practice, angles larger than 25 degrees result in images that
do not render correctly, therefore there is a limit of 25 degrees
for this function.
If this function returns images that are not rendered correctly, then
you must reduce the :attr:`max_left_rotation` and
:attr:`max_right_rotation` arguments!
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:param max_left_rotation: The maximum number of degrees the image can
be rotated to the left.
:param max_right_rotation: The maximum number of degrees the image can
be rotated to the right.
:type probability: Float
:type max_left_rotation: Integer
:type max_right_rotation: Integer
:return: None
"""
if not 0 < probability <= 1:
raise ValueError(Pipeline._probability_error_text)
if not 0 <= max_left_rotation <= 25:
raise ValueError("The max_left_rotation argument must be between 0 and 25.")
if not 0 <= max_right_rotation <= 25:
raise ValueError("The max_right_rotation argument must be between 0 and 25.")
else:
self.add_operation(RotateRange(probability=probability, max_left_rotation=ceil(max_left_rotation),
max_right_rotation=ceil(max_right_rotation)))
def rotate_without_crop(self, probability, max_left_rotation, max_right_rotation, expand=False):
"""
Rotate an image without automatically cropping.
The :attr:`expand` parameter controls whether the image is enlarged
to contain the new rotated images, or if the image size is maintained
Defaults to :attr:`false` so that images maintain their dimensions
when using this function.
:param probability: A value between 0 and 1 representing the
probability that the operation should be performed.
:param max_left_rotation: The maximum number of degrees the image can
be rotated to the left.
:param max_right_rotation: The maximum number of degrees the image can
be rotated to the right.
:type probability: Float
:type max_left_rotation: Integer
:type max_right_rotation: Integer
:param expand: Controls whether the image's size should be
increased to accommodate the rotation. Defaults to :attr:`false`
so that images maintain their original dimensions after rotation.
:return: None
"""
self.add_operation(RotateStandard(probability=probability, max_left_rotation=ceil(max_left_rotation),
max_right_rotation=ceil(max_right_rotation), expand=expand))