From f677af20e49d88a8ec89dd3daa1e97e9456780cf Mon Sep 17 00:00:00 2001 From: Greg Heinrich Date: Sun, 12 Jun 2016 16:22:18 +0200 Subject: [PATCH] Make padding optional in object detection data extension This change makes it optional to pad images when creating an object detection dataset. A form validator is added to verify that either both or none of width/height are specified when validating the form. Doing this in the form validator instead of (like formerly) the extension instanciation helps drop a more user-friendly error. Template updated to show width/height on same row. --- .../extensions/data/objectDetection/data.py | 48 +++++-------------- .../extensions/data/objectDetection/forms.py | 18 +++---- .../data/objectDetection/template.html | 44 +++++++++-------- .../extensions/data/objectDetection/utils.py | 27 ++++++++++- digits/utils/forms.py | 25 ++++++++++ 5 files changed, 97 insertions(+), 65 deletions(-) diff --git a/digits/extensions/data/objectDetection/data.py b/digits/extensions/data/objectDetection/data.py index bb5bddebe..a5ad647ad 100644 --- a/digits/extensions/data/objectDetection/data.py +++ b/digits/extensions/data/objectDetection/data.py @@ -15,7 +15,7 @@ from ..interface import DataIngestionInterface from .forms import DatasetForm from .utils import GroundTruth, GroundTruthObj -from .utils import bbox_to_array, resize_bbox_list +from .utils import bbox_to_array, pad_image, resize_bbox_list TEMPLATE = "template.html" @@ -41,12 +41,6 @@ def __init__(self, **kwargs): else: self.class_mappings = None - if ((self.val_image_folder == '') ^ (self.val_label_folder == '')): - raise ValueError("You must specify either both val_image_folder and val_label_folder or none") - - if ((self.resize_image_width is None) ^ (self.resize_image_height is None)): - raise ValueError("You must specify either both resize_image_width and resize_image_height or none") - # this will be set when we know the phase we are encoding self.ground_truth = None @@ -59,20 +53,26 @@ def encode_entry(self, entry): # (1) image part - # load from file + # load from file (this returns a PIL image) img = digits.utils.image.load_image(image_filename) if self.channel_conversion != 'none': if img.mode != self.channel_conversion: # convert to different image mode if necessary img = img.convert(self.channel_conversion) - # pad - img = self.pad_image(img) + # note: the form validator ensured that either none + # or both width/height were specified + if self.padding_image_width: + # pad image + img = pad_image( + img, + self.padding_image_height, + self.padding_image_width) if self.resize_image_width is not None: - # resize - resize_ratio_x = float(self.resize_image_width) / self.padding_image_width - resize_ratio_y = float(self.resize_image_height) / self.padding_image_height + resize_ratio_x = float(self.resize_image_width) / img.size[0] + resize_ratio_y = float(self.resize_image_height) / img.size[1] + # resize and convert to numpy HWC img = digits.utils.image.resize_image( img, self.resize_image_height, @@ -234,25 +234,3 @@ def make_image_list(self, folder): # shuffle random.shuffle(image_files) return image_files - - def pad_image(self, img): - """ - pad a single image to the dimensions specified in form - """ - src_width = img.size[0] - src_height = img.size[1] - - if self.padding_image_width < src_width: - raise ValueError("Source image width %d is greater than padding width %d" % (src_width, self.padding_image_width)) - - if self.padding_image_height < src_height: - raise ValueError("Source image height %d is greater than padding height %d" % (src_height, self.padding_image_height)) - - padded_img = PIL.Image.new( - img.mode, - (self.padding_image_width, self.padding_image_height), - "black") - - padded_img.paste(img, (0, 0)) # copy to top-left corner - - return padded_img diff --git a/digits/extensions/data/objectDetection/forms.py b/digits/extensions/data/objectDetection/forms.py index be8feb4d7..7c6f9ff28 100644 --- a/digits/extensions/data/objectDetection/forms.py +++ b/digits/extensions/data/objectDetection/forms.py @@ -7,7 +7,7 @@ from digits import utils from digits.utils import subclass - +from digits.utils.forms import validate_required_if_set @subclass class DatasetForm(Form): @@ -46,7 +46,7 @@ def validate_folder_path(form, field): val_image_folder = utils.forms.StringField( u'Validation image folder', validators=[ - validators.Optional(), + validate_required_if_set('val_label_folder'), validate_folder_path, ], tooltip="Indicate a folder of images to use for training" @@ -55,7 +55,7 @@ def validate_folder_path(form, field): val_label_folder = utils.forms.StringField( u'Validation label folder', validators=[ - validators.Optional(), + validate_required_if_set('val_image_folder'), validate_folder_path, ], tooltip="Indicate a folder of validation labels" @@ -64,7 +64,7 @@ def validate_folder_path(form, field): resize_image_width = utils.forms.IntegerField( u'Resize Image Width', validators=[ - validators.Optional(), + validate_required_if_set('resize_image_height'), validators.NumberRange(min=1), ], tooltip="If specified, images will be resized to that dimension after padding" @@ -73,7 +73,7 @@ def validate_folder_path(form, field): resize_image_height = utils.forms.IntegerField( u'Resize Image Height', validators=[ - validators.Optional(), + validate_required_if_set('resize_image_width'), validators.NumberRange(min=1), ], tooltip="If specified, images will be resized to that dimension after padding" @@ -83,20 +83,20 @@ def validate_folder_path(form, field): u'Padding Image Width', default=1248, validators=[ - validators.DataRequired(), + validate_required_if_set('padding_image_height'), validators.NumberRange(min=1), ], - tooltip="Images will be padded to that dimension" + tooltip="If specified, images will be padded to that dimension" ) padding_image_height = utils.forms.IntegerField( u'Padding Image Height', default=384, validators=[ - validators.DataRequired(), + validate_required_if_set('padding_image_width'), validators.NumberRange(min=1), ], - tooltip="Images will be padded to that dimension" + tooltip="If specified, images will be padded to that dimension" ) channel_conversion = utils.forms.SelectField( diff --git a/digits/extensions/data/objectDetection/template.html b/digits/extensions/data/objectDetection/template.html index 7699ea73b..188444439 100644 --- a/digits/extensions/data/objectDetection/template.html +++ b/digits/extensions/data/objectDetection/template.html @@ -39,28 +39,32 @@

Object Detection Dataset Options

{{ form.val_label_folder(class='form-control autocomplete_path', placeholder='folder')}} -
- {{ form.padding_image_width.label }} - {{ form.padding_image_width.tooltip }} - {{ form.padding_image_width(class='form-control')}} +
+ + +
+ {{ form.padding_image_width(size=4, placeholder='width', class='form-control') }} + x + {{ form.padding_image_height(size=4, placeholder='height', class='form-control') }} +
-
- {{ form.padding_image_height.label }} - {{ form.padding_image_height.tooltip }} - {{ form.padding_image_height(class='form-control')}} -
- -
- {{ form.resize_image_width.label }} - {{ form.resize_image_width.tooltip }} - {{ form.resize_image_width(class='form-control')}} -
- -
- {{ form.resize_image_height.label }} - {{ form.resize_image_height.tooltip }} - {{ form.resize_image_height(class='form-control')}} +
+ + +
+ {{ form.resize_image_width(size=4, placeholder='width', class='form-control') }} + x + {{ form.resize_image_height(size=4, placeholder='height', class='form-control') }} +
diff --git a/digits/extensions/data/objectDetection/utils.py b/digits/extensions/data/objectDetection/utils.py index 9a00ced3d..91f89b104 100644 --- a/digits/extensions/data/objectDetection/utils.py +++ b/digits/extensions/data/objectDetection/utils.py @@ -1,9 +1,11 @@ # Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. import csv -import numpy as np import os +import numpy as np +import PIL.Image + class ObjectType: Dontcare, Car, Van, Truck, Bus, Pickup, VehicleWithTrailer, SpecialVehicle,\ @@ -268,6 +270,29 @@ def bbox_overlap(abox, bbox): return overlap_pix, overlap_box + +def pad_image(img, padding_image_height, padding_image_width): + """ + pad a single image to the specified dimensions + """ + src_width = img.size[0] + src_height = img.size[1] + + if padding_image_width < src_width: + raise ValueError("Source image width %d is greater than padding width %d" % (src_width, padding_image_width)) + + if padding_image_height < src_height: + raise ValueError("Source image height %d is greater than padding height %d" % (src_height, padding_image_height)) + + padded_img = PIL.Image.new( + img.mode, + (padding_image_width, padding_image_height), + "black") + padded_img.paste(img, (0, 0)) # copy to top-left corner + + return padded_img + + def resize_bbox_list(bboxlist, rescale_x=1, rescale_y=1): # this is expecting x1,y1,w,h: bboxListNew = [] diff --git a/digits/utils/forms.py b/digits/utils/forms.py index 2e4fd227d..bdc91ecd2 100644 --- a/digits/utils/forms.py +++ b/digits/utils/forms.py @@ -38,6 +38,31 @@ def _validator(form, field): return _validator +def validate_required_if_set(other_field, **kwargs): + """ + Used as a validator within a wtforms.Form + + This implements a conditional DataRequired + `other_field` is a field name; if set, the other field makes it mandatory + to set the field being tested + """ + def _validator(form, field): + other_field_value = getattr(form, other_field).data + if other_field_value is not None and other_field_value is not "": + # Verify that data exists + if field.data is None \ + or (isinstance(field.data, (str, unicode)) + and not field.data.strip()) \ + or (isinstance(field.data, FileStorage) + and not field.data.filename.strip()): + raise validators.ValidationError('This field is required if %s is set.' % other_field) + else: + # This field is not required, ignore other errors + field.errors[:] = [] + raise validators.StopValidation() + + return _validator + def validate_greater_than(fieldname): """ Compares the value of two fields the value of self is to be greater than the supplied field.