Skip to content
This repository has been archived by the owner on Jan 7, 2025. It is now read-only.

Commit

Permalink
Make padding optional in object detection data extension
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gheinrich committed Jun 17, 2016
1 parent 5f98e8e commit f677af2
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 65 deletions.
48 changes: 13 additions & 35 deletions digits/extensions/data/objectDetection/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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
18 changes: 9 additions & 9 deletions digits/extensions/data/objectDetection/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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(
Expand Down
44 changes: 24 additions & 20 deletions digits/extensions/data/objectDetection/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,32 @@ <h3>Object Detection Dataset Options</h3>
{{ form.val_label_folder(class='form-control autocomplete_path', placeholder='folder')}}
</div>

<div class="form-group{{mark_errors([form.padding_image_width])}}">
{{ form.padding_image_width.label }}
{{ form.padding_image_width.tooltip }}
{{ form.padding_image_width(class='form-control')}}
<div class="form-group{{mark_errors([form.padding_image_width, form.padding_image_height])}}">
<label>Pad image (Width x Height)</label>
<span name="pad_dims_explanation"
class="explanation-tooltip glyphicon glyphicon-question-sign"
data-container="body"
title="If specified, input images will be padded to that dimension. Pad dimensions should be greater than those of input images."
></span>
<div class="input-group">
{{ form.padding_image_width(size=4, placeholder='width', class='form-control') }}
<span class="input-group-addon">x</span>
{{ form.padding_image_height(size=4, placeholder='height', class='form-control') }}
</div>
</div>

<div class="form-group{{mark_errors([form.padding_image_height])}}">
{{ form.padding_image_height.label }}
{{ form.padding_image_height.tooltip }}
{{ form.padding_image_height(class='form-control')}}
</div>

<div class="form-group{{mark_errors([form.resize_image_width])}}">
{{ form.resize_image_width.label }}
{{ form.resize_image_width.tooltip }}
{{ form.resize_image_width(class='form-control')}}
</div>

<div class="form-group{{mark_errors([form.resize_image_height])}}">
{{ form.resize_image_height.label }}
{{ form.resize_image_height.tooltip }}
{{ form.resize_image_height(class='form-control')}}
<div class="form-group{{mark_errors([form.resize_image_width, form.resize_image_height])}}">
<label>Resize image (Width x Height)</label>
<span name="resize_dims_explanation"
class="explanation-tooltip glyphicon glyphicon-question-sign"
data-container="body"
title="If specified, input images will be squashed to that dimension after padding."
></span>
<div class="input-group">
{{ form.resize_image_width(size=4, placeholder='width', class='form-control') }}
<span class="input-group-addon">x</span>
{{ form.resize_image_height(size=4, placeholder='height', class='form-control') }}
</div>
</div>

<div class="form-group{{mark_errors([form.channel_conversion])}}">
Expand Down
27 changes: 26 additions & 1 deletion digits/extensions/data/objectDetection/utils.py
Original file line number Diff line number Diff line change
@@ -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,\
Expand Down Expand Up @@ -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 = []
Expand Down
25 changes: 25 additions & 0 deletions digits/utils/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit f677af2

Please sign in to comment.