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

Bug Fix in Image Generator - Now Allows FCN's to be used #5228

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions keras/preprocessing/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,17 @@ def flip_axis(x, axis):
return x


def array_to_img(x, dim_ordering='default', scale=True):
def array_to_img(x, dim_ordering='default', scale=True, color_mode='rgb'):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add an argument you should document it in the docstring.

"""Converts a 3D Numpy array to a PIL Image instance.

# Arguments
x: Input Numpy array.
dim_ordering: Image data format.
scale: Whether to rescale image values
to be within [0, 255].
color_mode: 'bgr' to reverse the
channel dimensions, otherwise it picks
depending on the shape of X.

# Returns
A PIL Image instance.
Expand Down Expand Up @@ -232,6 +235,8 @@ def array_to_img(x, dim_ordering='default', scale=True):
x /= x_max
x *= 255
if x.shape[2] == 3:
if color_mode == 'bgr':
x = x[:, :, ::-1]
# RGB
return pil_image.fromarray(x.astype('uint8'), 'RGB')
elif x.shape[2] == 1:
Expand All @@ -241,12 +246,15 @@ def array_to_img(x, dim_ordering='default', scale=True):
raise ValueError('Unsupported channel number: ', x.shape[2])


def img_to_array(img, dim_ordering='default'):
def img_to_array(img, dim_ordering='default', color_mode='rgb'):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed both, please tell me if the docstring doesn't suffice.

"""Converts a PIL Image instance to a Numpy array.

# Arguments
img: PIL Image instance.
dim_ordering: Image data format.
color_mode: 'bgr' to reverse the
channel dimensions, otherwise it picks
depending on the shape of X.

# Returns
A 3D Numpy array (float32).
Expand All @@ -262,6 +270,8 @@ def img_to_array(img, dim_ordering='default'):
# or (channel, height, width)
# but original PIL image has format (width, height, channel)
x = np.asarray(img, dtype='float32')
if color_mode == 'bgr':
x = x[:, :, ::-1]
if len(x.shape) == 3:
if dim_ordering == 'th':
x = x.transpose(2, 0, 1)
Expand All @@ -275,7 +285,7 @@ def img_to_array(img, dim_ordering='default'):
return x


def load_img(path, grayscale=False, target_size=None):
def load_img(path, grayscale=False, target_size=(None, None)):
"""Loads an image into PIL format.

# Arguments
Expand All @@ -298,7 +308,7 @@ def load_img(path, grayscale=False, target_size=None):
img = img.convert('L')
else: # Ensure 3 channel even when loaded image is grayscale
img = img.convert('RGB')
if target_size:
if target_size[0] and target_size[1]: # Can't pass None in from iterator, hence tuple
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep the default to None and convert to (None, None) here. At the very least, target_size=None should keep being supported (for backwards compatibility).

img = img.resize((target_size[1], target_size[0]))
return img

Expand Down Expand Up @@ -742,9 +752,9 @@ def __init__(self, directory, image_data_generator,
self.directory = directory
self.image_data_generator = image_data_generator
self.target_size = tuple(target_size)
if color_mode not in {'rgb', 'grayscale'}:
if color_mode not in {'rgb', 'bgr', 'grayscale'}:
raise ValueError('Invalid color mode:', color_mode,
'; expected "rgb" or "grayscale".')
'; expected "rgb", "bgr" or "grayscale".')
self.color_mode = color_mode
self.dim_ordering = dim_ordering
if self.color_mode == 'rgb':
Expand Down Expand Up @@ -777,6 +787,9 @@ def __init__(self, directory, image_data_generator,
for subdir in sorted(os.listdir(directory)):
if os.path.isdir(os.path.join(directory, subdir)):
classes.append(subdir)
# User passed in None and no sub-folders, hence FCN. N.B.: Pass in classes=['.'] for improvements.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot quite understand this comment. Can you clarify?

Also: line too long.

if not classes:
classes = [directory]
self.nb_class = len(classes)
self.class_indices = dict(zip(classes, range(len(classes))))

Expand Down Expand Up @@ -822,22 +835,30 @@ def next(self):
index_array, current_index, current_batch_size = next(self.index_generator)
# The transformation of images is not under thread lock
# so it can be done in parallel
batch_x = np.zeros((current_batch_size,) + self.image_shape)
# If image_shape has None in position 1 it is intended to be used as a FCN
if self.image_shape[1]:
batch_x = np.zeros((current_batch_size,) + self.image_shape)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this assume all the images have the same shape?
If so, varying image dimensions should be handled, and some kind of target dimensions could be utilized to apply padding to the images so they are all the same size.

reference code:
https://github.com/aurora95/Keras-FCN/blob/370eb767df86f364d93eff9068a3edb0b194a18b/utils/SegDataGenerator.py#L221
https://github.com/nicolov/segmentation_keras/blob/master/utils/image_reader.py#L79

grayscale = self.color_mode == 'grayscale'
# build batch of image data
for i, j in enumerate(index_array):
fname = self.filenames[j]
img = load_img(os.path.join(self.directory, fname),
grayscale=grayscale,
target_size=self.target_size)
x = img_to_array(img, dim_ordering=self.dim_ordering)
x = img_to_array(img, dim_ordering=self.dim_ordering, color_mode=self.color_mode)
# Allows shape to be None, but batch size must equal one.
if not self.image_shape[1] and i == 0:
if current_batch_size != 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why must batch size be one? Just looking for clarification because I'm not 100% sure what this case is checking for because dense prediction tasks including FCNs can train with batch sizes >1.

raise ValueError('Current batch size is: %s , should be size one for current shape'
% current_batch_size)
batch_x = np.zeros((current_batch_size,) + x.shape)
x = self.image_data_generator.random_transform(x)
x = self.image_data_generator.standardize(x)
batch_x[i] = x
# optionally save augmented images to disk for debugging purposes
if self.save_to_dir:
for i in range(current_batch_size):
img = array_to_img(batch_x[i], self.dim_ordering, scale=True)
img = array_to_img(batch_x[i], self.dim_ordering, scale=True, color_mode=self.color_mode)
fname = '{prefix}_{index}_{hash}.{format}'.format(prefix=self.save_prefix,
index=current_index + i,
hash=np.random.randint(1e4),
Expand Down
27 changes: 27 additions & 0 deletions tests/keras/preprocessing/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,33 @@ def test_directory_iterator(self):
assert(sorted(dir_iterator.filenames) == sorted(filenames))
shutil.rmtree(tmp_folder)

''' Test for the directory iterator when we wish to use it for a FCN.'''
def test_directory_iterator_fcn(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest also checking if labels can be loaded from pngs without palette conversion as described in the following, perhaps an additonal color mode 'segmentation_label' would be appropriate:
nicolov/segmentation_keras#14

Though perhaps something like that is out of scope for this pull request.

num_classes = 1 # 1 class encompassing all masks
tmp_folder = tempfile.mkdtemp(prefix='test_images')

# no subfolders

# save the images in the paths
count = 0
filenames = []
for test_images in self.all_test_images:
for im in test_images:
filename = 'image-{}.jpg'.format(count)
filenames.append(filename)
im.save(os.path.join(tmp_folder, filename))
count += 1

# create iterator
generator = image.ImageDataGenerator()
dir_iterator = generator.flow_from_directory(tmp_folder, classes=['.'])

# check number of classes and images
assert (len(dir_iterator.class_indices) == num_classes)
assert (len(dir_iterator.classes) == count)
assert (sorted(dir_iterator.filenames) == sorted(filenames))
shutil.rmtree(tmp_folder)

def test_img_utils(self):
height, width = 10, 8

Expand Down