From 7aaeb5f62ed30ae218250e61138fdd7f233aaf47 Mon Sep 17 00:00:00 2001 From: Greg Heinrich Date: Wed, 31 Aug 2016 17:04:19 +0200 Subject: [PATCH 01/52] Modularization of inference If a dataset extension implements its get_inference_form() method then the standard image inference form is replaced with the extension's inference form. During inference, an inference dataset is created without the data from the inference form. --- digits/extensions/data/interface.py | 30 ++- digits/model/images/generic/views.py | 160 +++++++++++++++- .../images/generic/infer_extension.html | 176 ++++++++++++++++++ .../templates/models/images/generic/show.html | 24 +++ tools/inference.py | 3 +- 5 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 digits/templates/models/images/generic/infer_extension.html diff --git a/digits/extensions/data/interface.py b/digits/extensions/data/interface.py index 1fe346dda..f31b1055d 100644 --- a/digits/extensions/data/interface.py +++ b/digits/extensions/data/interface.py @@ -7,7 +7,16 @@ class DataIngestionInterface(object): A data ingestion extension """ - def __init__(self, **kwargs): + def __init__(self, is_inference_db=False, **kwargs): + """ + Initialize the data ingestion extension + Parameters: + - is_inference_db: boolean value, indicates whether the database is + created for inference. If this is true then the extension needs to + use the data from the inference form and create a database only for + the test phase (stage == constants.TEST_DB) + - kwargs: dataset form fields + """ # save all data there - no other fields will be persisted self.userdata = kwargs @@ -58,19 +67,24 @@ def get_id(): """ raise NotImplementedError - @staticmethod - def get_inference_form(): + def get_inference_form(self): """ - For later use + Return a Form object with all fields required to create an inference dataset """ - raise NotImplementedError + return None @staticmethod - def get_inference_template(): + def get_inference_template(form): """ - For later use + Parameters: + - form: form returned by get_inference_form(). + return: + - (template, context) tuple + - template is a Jinja template to use for rendering the inference form + - context is a dictionary of context variables to use for rendering + the form """ - raise NotImplementedError + return (None, None) @staticmethod def get_title(): diff --git a/digits/model/images/generic/views.py b/digits/model/images/generic/views.py index c7b9161ee..e74569cf9 100644 --- a/digits/model/images/generic/views.py +++ b/digits/model/images/generic/views.py @@ -17,6 +17,7 @@ from digits.inference import ImageInferenceJob from digits.status import Status from digits.utils import filesystem as fs +from digits.utils import constants from digits.utils.forms import fill_form_if_cloned, save_form_to_job from digits.utils.routing import get_request_arg, request_wants_json, job_from_request from digits.webapp import scheduler @@ -289,11 +290,29 @@ def show(job, related_jobs=None): Called from digits.model.views.models_show() """ view_extensions = get_view_extensions() + + inference_form_html = None + if isinstance(job.dataset, GenericDatasetJob): + extension_class = extensions.data.get_extension(job.dataset.extension_id) + if not extension_class: + raise RuntimeError("Unable to find data extension with ID=%s" + % job.dataset.extension_id) + extension_userdata = job.dataset.extension_userdata + extension_userdata.update({'is_inference_db':True}) + extension = extension_class(**extension_userdata) + + form = extension.get_inference_form() + if form: + template, context = extension.get_inference_template(form) + inference_form_html = flask.render_template_string(template, **context) + return flask.render_template( 'models/images/generic/show.html', job=job, view_extensions=view_extensions, - related_jobs=related_jobs) + related_jobs=related_jobs, + inference_form_html=inference_form_html, + ) @blueprint.route('/large_graph', methods=['GET']) @@ -400,6 +419,97 @@ def infer_one(): ), status_code +@blueprint.route('/infer_extension.json', methods=['POST']) +@blueprint.route('/infer_extension', methods=['POST', 'GET']) +def infer_extension(): + """ + Perform inference using the data from an extension inference form + """ + model_job = job_from_request() + + inference_db_job = None + try: + # create an inference database + inference_db_job = create_inference_db(model_job) + db_path = inference_db_job.get_feature_db_path(constants.TEST_DB) + + # create database creation job + epoch = None + if 'snapshot_epoch' in flask.request.form: + epoch = float(flask.request.form['snapshot_epoch']) + + layers = 'none' + if 'show_visualizations' in flask.request.form and flask.request.form['show_visualizations']: + layers = 'all' + + # create inference job + inference_job = ImageInferenceJob( + username=utils.auth.get_username(), + name="Inference", + model=model_job, + images=db_path, + epoch=epoch, + layers=layers, + resize=False, + ) + + # schedule tasks + scheduler.add_job(inference_job) + + # wait for job to complete + inference_job.wait_completion() + + finally: + if inference_db_job: + scheduler.delete_job(inference_db_job) + + # retrieve inference data + inputs, outputs, model_visualization = inference_job.get_data() + + # set return status code + status_code = 500 if inference_job.status == 'E' else 200 + + # delete job folder and remove from scheduler list + scheduler.delete_job(inference_job) + + if outputs is not None and len(outputs) < 1: + # an error occurred + outputs = None + + if inputs is not None: + keys = [str(idx) for idx in inputs['ids']] + inference_views_html, header_html, app_begin_html, app_end_html = get_inference_visualizations( + model_job.dataset, + inputs, + outputs) + else: + inference_views_html = None + header_html = None + keys = None + app_begin_html = None + app_end_html = None + + if request_wants_json(): + result = {} + for i, key in enumerate(keys): + result[key] = dict((name, blob[i].tolist()) for name,blob in outputs.iteritems()) + return flask.jsonify({'outputs': result}), status_code + else: + return flask.render_template( + 'models/images/generic/infer_extension.html', + model_job=model_job, + job=inference_job, + keys=keys, + inference_views_html=inference_views_html, + header_html=header_html, + app_begin_html=app_begin_html, + app_end_html=app_end_html, + visualizations=model_visualization, + total_parameters=sum(v['param_count'] for v in model_visualization + if v['vis_type'] == 'Weights'), + ), status_code + + @blueprint.route('/infer_db.json', methods=['POST']) @blueprint.route('/infer_db', methods=['POST', 'GET']) def infer_db(): @@ -601,6 +711,54 @@ def infer_many(): ), status_code +def create_inference_db(model_job): + # create instance of extension class + extension_class = extensions.data.get_extension(model_job.dataset.extension_id) + extension_userdata = model_job.dataset.extension_userdata + extension_userdata.update({'is_inference_db':True}) + extension = extension_class(**extension_userdata) + + extension_form = extension.get_inference_form() + extension_form_valid = extension_form.validate_on_submit() + + if not extension_form_valid: + errors = extension_form.errors.copy() + raise werkzeug.exceptions.BadRequest(repr(errors)) + + extension.userdata.update(extension_form.data) + + # create job + job = GenericDatasetJob( + username=utils.auth.get_username(), + name='Inference dataset', + group=None, + backend='lmdb', + feature_encoding='none', + label_encoding='none', + batch_size=1, + num_threads=1, + force_same_shape=0, + extension_id=model_job.dataset.extension_id, + extension_userdata=extension.get_user_data(), + ) + + # schedule tasks and wait for job to complete + scheduler.add_job(job) + job.wait_completion() + + # check for errors + if job.status != Status.DONE: + msg = "" + for task in job.tasks: + if task.exception: + msg = msg + task.exception + if task.traceback: + msg = msg + task.exception + raise RuntimeError(msg) + + return job + + def get_datasets(extension_id): if extension_id: jobs = [j for j in scheduler.jobs.values() diff --git a/digits/templates/models/images/generic/infer_extension.html b/digits/templates/models/images/generic/infer_extension.html new file mode 100644 index 000000000..5dc543461 --- /dev/null +++ b/digits/templates/models/images/generic/infer_extension.html @@ -0,0 +1,176 @@ +{# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved. #} +{% extends "job.html" %} + +{% block head %} + + +{% endblock %} + +{% block nav %} +
  • {{model_job.name()}}
  • +
  • Test Many
  • +{% endblock %} + +{% block job_content %} + + + +{% if not keys %} +
    +

    Inference failed, see job log

    +
    +{% endif %} + +{% endblock %} + +{% block job_content_details %} + +{% if header_html %} +
    +

    Summary

    + {{header_html|safe}} +
    +{% endif %} + +{% if app_begin_html %} +{{app_begin_html|safe}} +{% endif %} + +{% if inference_views_html %} +

    Output visualizations

    + {% if keys|length > 1 %} +
    + + + + + + {% for key in keys %} + + + + + {% endfor %} +
    IndexData
    {{loop.index}} + {% set index=loop.index0 %} + {{ inference_views_html[index]|safe }} +
    +
    + {% else %} + {{ inference_views_html[0]|safe }} + {% endif %} +{% endif %} + + + + +{% if visualizations %} +

    Layer visualizations

    + + + + + + + {% for vis in visualizations %} + + + + + + {% endfor %} + + + + +
    DescriptionStatisticsVisualization
    +

    "{{vis['name']}}"

    +

    + {{vis['vis_type']}} + {% if 'layer_type' in vis %} + ({{vis['layer_type']}} layer) + {% endif %} +

    + {% if 'param_count' in vis %} +

    + {{'{:,}'.format(vis['param_count'])}} learned parameters +

    + {% endif %} +
    + Data shape: {{vis['data_stats']['shape']}}
    + Mean: {{vis['data_stats']['mean']}}
    + Std deviation: {{vis['data_stats']['stddev']}}
    +
    + +
    + {% if vis['image_html'] %} + + + {% else %} + Not shown + {% endif %} +
    Totals + Total learned parameters: + {{'{:,}'.format(total_parameters)}} +
    +{% endif %} + +{% if app_end_html %} +{{app_end_html|safe}} +{% endif %} + +{% endblock %} + diff --git a/digits/templates/models/images/generic/show.html b/digits/templates/models/images/generic/show.html index 9856c09b1..450e44b36 100644 --- a/digits/templates/models/images/generic/show.html +++ b/digits/templates/models/images/generic/show.html @@ -208,6 +208,29 @@

    Inference Options

    +{% if inference_form_html %} +{{ inference_form_html|safe }} +
    + + +
    + + +{% else %}

    Test a single image

    @@ -315,6 +338,7 @@

    Test a list of images

    >
    +{% endif %} diff --git a/tools/inference.py b/tools/inference.py index d33614fb5..a008b51f7 100755 --- a/tools/inference.py +++ b/tools/inference.py @@ -159,7 +159,8 @@ def infer(input_list, gpu=gpu, resize=resize) else: - assert layers == 'none' + if layers != 'none': + raise InferenceError("Layer visualization is not supported for multiple inference") outputs = model.train_task().infer_many( input_data, snapshot_epoch=epoch, From 64a1781442236b6c8cd21384aa0d23ac7bb11a0a Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Mon, 12 Sep 2016 11:44:25 -0700 Subject: [PATCH 02/52] Move digits.__version__ into new file; add tests --- digits/__init__.py | 5 ++++- digits/test_version.py | 49 ++++++++++++++++++++++++++++++++++++++++++ digits/version.py | 3 +++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 digits/test_version.py create mode 100644 digits/version.py diff --git a/digits/__init__.py b/digits/__init__.py index 520870ac9..0517370b6 100644 --- a/digits/__init__.py +++ b/digits/__init__.py @@ -1 +1,4 @@ -__version__ = '4.1-dev' +# Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. +from __future__ import absolute_import + +from .version import __version__ diff --git a/digits/test_version.py b/digits/test_version.py new file mode 100644 index 000000000..0432daa30 --- /dev/null +++ b/digits/test_version.py @@ -0,0 +1,49 @@ +# Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. +from __future__ import absolute_import + +import os.path +import re + + +class TestVersion(): + DEV_REGEX = re.compile('^(0|[1-9]\d*)\.(0|[1-9]\d*)-dev$') + + # Copyright (c) Sindre Sorhus (sindresorhus.com) + # The MIT License (MIT) + # https://github.com/sindresorhus/semver-regex/blob/v1.0.0/index.js + STANDARD_SEMVER_REGEX = re.compile( + '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' + '(-[\da-z\-]+(\.[\da-z\-]+)*)?(\+[\da-z\-]+(\.[\da-z\-]+)*)?$') + + def check_version(self, version): + standard_match = re.match(self.STANDARD_SEMVER_REGEX, version) + dev_match = re.match(self.DEV_REGEX, version) + assert (standard_match is not None or dev_match is not None), \ + 'Version string "%s" is ill-formatted' % version + + def test_package_version(self): + import digits + self.check_version(digits.__version__) + + def test_import_version(self): + import digits.version + self.check_version(digits.version.__version__) + + # Test a programmatic and reliable way to check the version + # python -c "execfile('digits/version.py'); print __version__" + def test_execfile_version(self): + import digits + filename = os.path.join(os.path.dirname(digits.__file__), 'version.py') + file_locals = {} + execfile(filename, {}, file_locals) + assert file_locals.keys() == ['__version__'], \ + 'version.py should only declare a single variable' + self.check_version(file_locals['__version__']) + + # Make sure somebody doesn't overwrite the version in __init__.py + def test_package_version_matches_import_version(self): + import digits + import digits.version + + assert digits.__version__ == digits.version.__version__ + diff --git a/digits/version.py b/digits/version.py new file mode 100644 index 000000000..5841ba39e --- /dev/null +++ b/digits/version.py @@ -0,0 +1,3 @@ +# Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. + +__version__ = '4.1-dev' From 6ae5a7dfbd0c249de666d9f29c28b62f3915f7dd Mon Sep 17 00:00:00 2001 From: Gregory Heinrich Date: Thu, 8 Sep 2016 00:43:05 +0100 Subject: [PATCH 03/52] Add tests for modularization of inference --- digits/extensions/data/imageGradients/data.py | 44 ++++++++++++++----- .../extensions/data/imageGradients/forms.py | 35 +++++++++++++++ .../imageGradients/inference_template.html | 34 ++++++++++++++ digits/model/images/generic/test_views.py | 15 +++++++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 digits/extensions/data/imageGradients/inference_template.html diff --git a/digits/extensions/data/imageGradients/data.py b/digits/extensions/data/imageGradients/data.py index 4c3b772ac..d51c1f77b 100644 --- a/digits/extensions/data/imageGradients/data.py +++ b/digits/extensions/data/imageGradients/data.py @@ -3,13 +3,13 @@ from digits.utils import subclass, override, constants from ..interface import DataIngestionInterface -from .forms import DatasetForm +from .forms import DatasetForm, InferenceForm import numpy as np import os TEMPLATE = "template.html" - +INFERENCE_TEMPLATE = "inference_template.html" @subclass class DataIngestion(DataIngestionInterface): @@ -17,16 +17,18 @@ class DataIngestion(DataIngestionInterface): A data ingestion extension for an image gradient dataset """ - def __init__(self, **kwargs): + def __init__(self, is_inference_db=False, **kwargs): super(DataIngestion, self).__init__(**kwargs) + self.userdata['is_inference_db'] = is_inference_db + # Used to calculate the gradients later self.yy, self.xx = np.mgrid[:self.image_height, :self.image_width].astype('float') @override def encode_entry(self, entry): - xslope, yslope = np.random.random_sample(2) - 0.5 + xslope, yslope = entry label = np.array([xslope, yslope]) a = xslope * 255 / self.image_width b = yslope * 255 / self.image_height @@ -74,6 +76,18 @@ def get_dataset_template(form): context = {'form': form} return (template, context) + @override + def get_inference_form(self): + return InferenceForm() + + @staticmethod + @override + def get_inference_template(form): + extension_dir = os.path.dirname(os.path.abspath(__file__)) + template = open(os.path.join(extension_dir, INFERENCE_TEMPLATE), "r").read() + context = {'form': form} + return (template, context) + @staticmethod @override def get_title(): @@ -81,12 +95,18 @@ def get_title(): @override def itemize_entries(self, stage): - if stage == constants.TRAIN_DB: - count = self.train_image_count - elif stage == constants.VAL_DB: - count = self.val_image_count - elif stage == constants.TEST_DB: - count = self.test_image_count + count = 0 + if self.userdata['is_inference_db']: + if stage == constants.TEST_DB: + if self.test_image_count: + count = self.test_image_count + else: + return [(self.gradient_x, self.gradient_y)] else: - raise ValueError('Unknown stage %s' % stage) - return xrange(count) if count > 0 else [] + if stage == constants.TRAIN_DB: + count = self.train_image_count + elif stage == constants.VAL_DB: + count = self.val_image_count + elif stage == constants.TEST_DB: + count = self.test_image_count + return [np.random.random_sample(2) - 0.5 for i in xrange(count)] if count > 0 else [] diff --git a/digits/extensions/data/imageGradients/forms.py b/digits/extensions/data/imageGradients/forms.py index 03ee1064e..221c0b3b5 100644 --- a/digits/extensions/data/imageGradients/forms.py +++ b/digits/extensions/data/imageGradients/forms.py @@ -3,6 +3,7 @@ from digits import utils from digits.utils import subclass +from digits.utils.forms import validate_required_iff from flask.ext.wtf import Form import wtforms from wtforms import validators @@ -55,3 +56,37 @@ class DatasetForm(Form): default=32, validators=[validators.DataRequired()] ) + + +@subclass +class InferenceForm(Form): + """ + A form used to perform inference on a gradient regression model + """ + + gradient_x = utils.forms.FloatField( + 'Gradient (x)', + validators=[ + validate_required_iff(test_image_count=None), + validators.NumberRange(min=-0.5, max=0.5), + ], + tooltip="Specify a number between -0.5 and 0.5" + ) + + gradient_y = utils.forms.FloatField( + 'Gradient (y)', + validators=[ + validate_required_iff(test_image_count=None), + validators.NumberRange(min=-0.5, max=0.5), + ], + tooltip="Specify a number between -0.5 and 0.5" + ) + + test_image_count = utils.forms.IntegerField( + 'Test Image count', + validators=[ + validators.Optional(), + validators.NumberRange(min=0), + ], + tooltip="Number of images to create in test set" + ) diff --git a/digits/extensions/data/imageGradients/inference_template.html b/digits/extensions/data/imageGradients/inference_template.html new file mode 100644 index 000000000..864ac02a4 --- /dev/null +++ b/digits/extensions/data/imageGradients/inference_template.html @@ -0,0 +1,34 @@ +{# Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. #} + +{% from "helper.html" import print_flashes %} +{% from "helper.html" import print_errors %} +{% from "helper.html" import mark_errors %} + +
    +
    +

    Test an image

    +
    + {{ form.gradient_x.label }} + {{ form.gradient_x.tooltip }} + {{ form.gradient_x(class='form-control') }} +
    + +
    + {{ form.gradient_y.label }} + {{ form.gradient_y.tooltip }} + {{ form.gradient_y(class='form-control') }} +
    +
    + +
    +

    Test a list of images

    +
    +
    + {{ form.test_image_count.label }} + {{ form.test_image_count.tooltip }} + {{ form.test_image_count(class='form-control') }} +
    +
    +
    +
    + diff --git a/digits/model/images/generic/test_views.py b/digits/model/images/generic/test_views.py index b5e23de95..1b0747fdc 100644 --- a/digits/model/images/generic/test_views.py +++ b/digits/model/images/generic/test_views.py @@ -703,6 +703,21 @@ def setUpClass(cls, **kwargs): # note: model created in BaseTestCreatedWithAnyDataset.setUpClass method super(BaseTestCreatedWithGradientDataExtension, cls).setUpClass() + def test_infer_extension_json(self): + rv = self.app.post( + '/models/images/generic/infer_extension.json?job_id=%s' % self.model_id, + data = { + 'gradient_x': 0.5, + 'gradient_y': -0.5, + } + ) + assert rv.status_code == 200, 'POST failed with %s' % rv.status_code + data = json.loads(rv.data) + output = data['outputs'][data['outputs'].keys()[0]]['output'] + assert output[0] > 0 and \ + output[1] < 0, \ + 'image regression result is wrong: %s' % data['outputs']['output'] + class BaseTestCreatedWithImageProcessingExtension( BaseTestCreatedWithAnyDataset, From f577e3c06fe54403f671b294dc5b6d2b02124d11 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Tue, 13 Sep 2016 10:46:13 -0700 Subject: [PATCH 04/52] [ObjDet] Allow non-numeric filenames --- digits/extensions/data/objectDetection/data.py | 5 +---- digits/extensions/data/objectDetection/utils.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/digits/extensions/data/objectDetection/data.py b/digits/extensions/data/objectDetection/data.py index 2b90400fa..fc3d4ce45 100644 --- a/digits/extensions/data/objectDetection/data.py +++ b/digits/extensions/data/objectDetection/data.py @@ -97,10 +97,7 @@ def encode_entry(self, entry): # (2) label part # make sure label exists - try: - label_id = int(os.path.splitext(os.path.basename(entry))[0]) - except: - raise ValueError("Unable to extract numerical id from file name %s" % entry) + label_id = os.path.splitext(os.path.basename(entry))[0] if not label_id in self.datasrc_annotation_dict: raise ValueError("Label key %s not found in label folder" % label_id) diff --git a/digits/extensions/data/objectDetection/utils.py b/digits/extensions/data/objectDetection/utils.py index 91f89b104..31910e401 100644 --- a/digits/extensions/data/objectDetection/utils.py +++ b/digits/extensions/data/objectDetection/utils.py @@ -203,7 +203,7 @@ def load_gt_obj(self): gt.stype = '' gt.object = ObjectType.Dontcare objects_per_image.append(gt) - key = int(os.path.splitext(label_file)[0]) + key = os.path.splitext(label_file)[0] self.update_objects_all(key, objects_per_image) @property From db50481b7839aafebc34f13817d5d53c606d7c0a Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Tue, 13 Sep 2016 11:16:03 -0700 Subject: [PATCH 05/52] [ObjDet] Improve docs about case sensitivity --- digits/extensions/data/objectDetection/README.md | 3 ++- digits/extensions/data/objectDetection/forms.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/digits/extensions/data/objectDetection/README.md b/digits/extensions/data/objectDetection/README.md index c41b2bf1d..7ddb258a1 100644 --- a/digits/extensions/data/objectDetection/README.md +++ b/digits/extensions/data/objectDetection/README.md @@ -150,7 +150,8 @@ All classes which don't exist in the provided mapping are implicitly mapped to 0 DetectNet is a single-class object detection network, and only cares about the "Car" class, which is expected to be ID 1. You can change the mapping in the DetectNet prototxt, but it's simplest to just make the class you care about map to 1. -Custom class mappings may be used by specifiying a comma-separated list of lower-case class names in the Object Detection dataset creation form. +Custom class mappings may be used by specifiying a comma-separated list of class names in the Object Detection dataset creation form. +All labels are converted to lower-case, so the matching is case-insensitive. For example, if you only want to detect pedestrians, enter `dontcare,pedestrian` in the "Custom classes" field to generate this mapping: diff --git a/digits/extensions/data/objectDetection/forms.py b/digits/extensions/data/objectDetection/forms.py index 823287219..b4d389130 100644 --- a/digits/extensions/data/objectDetection/forms.py +++ b/digits/extensions/data/objectDetection/forms.py @@ -127,7 +127,7 @@ def validate_folder_path(form, field): validators=[ validators.Optional(), ], - tooltip="Enter a comma-separated list of lower-case class names. " + tooltip="Enter a comma-separated list of class names. " "Class IDs are assigned sequentially, starting from 0. " "Leave this field blank to use default class mappings. " "See object detection extension documentation for more " From 21b9fd6b81b455d35d08acfa6f20188d7eebf027 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Tue, 13 Sep 2016 11:17:12 -0700 Subject: [PATCH 06/52] [ObjDet] Improve docs about mapping to zero --- digits/extensions/data/objectDetection/README.md | 2 +- digits/extensions/data/objectDetection/forms.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/digits/extensions/data/objectDetection/README.md b/digits/extensions/data/objectDetection/README.md index 7ddb258a1..ca3bae154 100644 --- a/digits/extensions/data/objectDetection/README.md +++ b/digits/extensions/data/objectDetection/README.md @@ -160,4 +160,4 @@ Class name | Class ID dontcare | 0 pedestrian | 1 -All labeled objects other than "pedestrian" in your dataset will be mapped to 0. +All labeled objects other than "pedestrian" in your dataset will be mapped to 0, along with any objects explicitly labeled as "dontcare". diff --git a/digits/extensions/data/objectDetection/forms.py b/digits/extensions/data/objectDetection/forms.py index b4d389130..5c9ceb890 100644 --- a/digits/extensions/data/objectDetection/forms.py +++ b/digits/extensions/data/objectDetection/forms.py @@ -129,6 +129,7 @@ def validate_folder_path(form, field): ], tooltip="Enter a comma-separated list of class names. " "Class IDs are assigned sequentially, starting from 0. " + "Unmapped class names automatically map to 0. " "Leave this field blank to use default class mappings. " "See object detection extension documentation for more " "information." From a2584962cf53a3817ad4824969b504a1482e8d87 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Tue, 13 Sep 2016 13:09:58 -0700 Subject: [PATCH 07/52] [ObjDet] Improve handling of label files Specifically, empty or invalid files --- digits/extensions/data/objectDetection/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/digits/extensions/data/objectDetection/utils.py b/digits/extensions/data/objectDetection/utils.py index 31910e401..3f4a2e795 100644 --- a/digits/extensions/data/objectDetection/utils.py +++ b/digits/extensions/data/objectDetection/utils.py @@ -177,6 +177,12 @@ def load_gt_obj(self): objects_per_image = list() with open( os.path.join(self.label_dir, label_file), 'rb') as flabel: for row in csv.reader(flabel, delimiter=self.label_delimiter): + if len(row) == 0: + # This can happen when you open an empty file + continue + if len(row) < 15: + raise ValueError('Invalid label format in "%s"' + % os.path.join(self.label_dir, label_file)) # load data gt = GroundTruthObj() @@ -203,7 +209,7 @@ def load_gt_obj(self): gt.stype = '' gt.object = ObjectType.Dontcare objects_per_image.append(gt) - key = os.path.splitext(label_file)[0] + key = os.path.splitext(label_file)[0] self.update_objects_all(key, objects_per_image) @property From 4447eeee1cccb25ac44142b25e5f5236befc0b72 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Wed, 14 Sep 2016 09:18:30 -0700 Subject: [PATCH 08/52] Gunicorn fix - add abspath(curdir) to PATH --- gunicorn_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunicorn_config.py b/gunicorn_config.py index 0a81c51d6..de41839d8 100644 --- a/gunicorn_config.py +++ b/gunicorn_config.py @@ -4,7 +4,7 @@ import sys # Add path to digits module -sys.path.append(os.path.dirname(__file__)) +sys.path.append(os.path.abspath(os.path.dirname(__file__))) worker_class = 'geventwebsocket.gunicorn.workers.GeventWebSocketWorker' bind = '0.0.0.0:34448' # DIGIT From 5edc6eefc2cde432ac3811b485ab01e7c49046c6 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Wed, 14 Sep 2016 18:59:41 -0700 Subject: [PATCH 09/52] [ObjDet] Remove debug print --- digits/extensions/view/boundingBox/view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/digits/extensions/view/boundingBox/view.py b/digits/extensions/view/boundingBox/view.py index 1cfa1c9f0..b58fefb1b 100644 --- a/digits/extensions/view/boundingBox/view.py +++ b/digits/extensions/view/boundingBox/view.py @@ -121,7 +121,6 @@ def process_data( self.bbox_count += len(bboxes[key]) image_html = digits.utils.image.embed_image_html(image) - print bboxes return { 'image': image_html, 'bboxes': bboxes, From ed37d214656b786b321b856ffa51a7ccf40117ac Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 10:52:49 -0700 Subject: [PATCH 10/52] Supress numpy VisibleDeprecationWarning --- digits/utils/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/digits/utils/image.py b/digits/utils/image.py index 242aef5da..88decefe3 100644 --- a/digits/utils/image.py +++ b/digits/utils/image.py @@ -105,7 +105,7 @@ def upscale(image, ratio): channels = image.shape[2] out = np.ndarray((height, width, channels),dtype=np.uint8) for x, y in np.ndindex((width,height)): - out[y,x] = image[math.floor(y/ratio), math.floor(x/ratio)] + out[y,x] = image[int(math.floor(y/ratio)), int(math.floor(x/ratio))] return out From cb9bb365856e5e721811c632672abd0c5467728e Mon Sep 17 00:00:00 2001 From: James Date: Thu, 15 Sep 2016 15:48:41 -0400 Subject: [PATCH 11/52] cookie.tmp replaced by digits.cookie --- docs/API.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/API.md b/docs/API.md index f2d7fdde9..bf515bdad 100644 --- a/docs/API.md +++ b/docs/API.md @@ -43,21 +43,21 @@ In order to create a dataset, you will first need to log in. The following command will log us in as user `fred`: ```sh -$ curl localhost/login -c cookie.tmp -XPOST -F username=fred +$ curl localhost/login -c digits.cookie -XPOST -F username=fred Redirecting...

    Redirecting...

    You should be redirected automatically to target URL: /. If not click the link.(venv) ``` -Note the `-c cookie.tmp` flag, which instructs `curl` to store the session cookie into `cookie.tmp`. +Note the `-c digits.cookie` flag, which instructs `curl` to store the session cookie into `digits.cookie`. DIGITS requires users to log in before creating jobs. A job can only be edited or deleted by the user that created it. The session cookie is required for all commands that create or modify jobs. -For those commands we will use `-b cookie.tmp` in the `curl` command line to pass the session cookie to DIGITS. +For those commands we will use `-b digits.cookie` in the `curl` command line to pass the session cookie to DIGITS. > NOTE: if you prefer not to store cookies you may to use the `username` hidden form field directly. -> This may be done by replacing `-b cookie.tmp` with `-F username=fred` in the commands that require authentication. +> This may be done by replacing `-b digits.cookie` with `-F username=fred` in the commands that require authentication. > Using cookies would however be more robust to future changes in the authentication scheme. In the above command `/login` is referred to as a "route". From 0fa55cda68f0be7d7c67f3a34935aa31da53b6cc Mon Sep 17 00:00:00 2001 From: Joe Mancewicz Date: Thu, 15 Sep 2016 12:53:36 -0700 Subject: [PATCH 12/52] Changed a few things so that IE would display the front page correctly --- digits/static/js/home_app.js | 2 +- digits/templates/layout.html | 3 +++ digits/templates/partials/home/model_pane.html | 4 ++-- digits/templates/partials/home/pretrained_model_pane.html | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/digits/static/js/home_app.js b/digits/static/js/home_app.js index cd92c30a0..6b76c39e8 100644 --- a/digits/static/js/home_app.js +++ b/digits/static/js/home_app.js @@ -12,7 +12,7 @@ try { }); app.controller('tab_controller', function ($scope) { - self = this; + var self = this; $scope.init = function(tab){ self.tab = _.isUndefined(tab) ? 2 : tab; }; diff --git a/digits/templates/layout.html b/digits/templates/layout.html index 1a672ae8e..c9e3e8a0e 100644 --- a/digits/templates/layout.html +++ b/digits/templates/layout.html @@ -2,6 +2,9 @@ + + + diff --git a/digits/templates/partials/home/model_pane.html b/digits/templates/partials/home/model_pane.html index 0a95b9df8..932bb82c7 100644 --- a/digits/templates/partials/home/model_pane.html +++ b/digits/templates/partials/home/model_pane.html @@ -115,13 +115,13 @@ ng-keydown="keydown($event)" ng-class="{selected:job.selected}"> - + {[ job.id ]} - + diff --git a/digits/templates/partials/home/pretrained_model_pane.html b/digits/templates/partials/home/pretrained_model_pane.html index 8ea23c151..1f5185c66 100644 --- a/digits/templates/partials/home/pretrained_model_pane.html +++ b/digits/templates/partials/home/pretrained_model_pane.html @@ -86,9 +86,9 @@ ng-keydown="keydown($event)" ng-class="{selected:job.selected}"> - + - + {[ job.status ]} From 575d23a829dfd0d4a6e48d95ad63c22f4564084a Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 13:15:23 -0700 Subject: [PATCH 13/52] Install a few more deb packages on TravisCI --- scripts/travis/install-apt.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/travis/install-apt.sh b/scripts/travis/install-apt.sh index c78af372e..531146e27 100755 --- a/scripts/travis/install-apt.sh +++ b/scripts/travis/install-apt.sh @@ -29,8 +29,10 @@ sudo apt-get install -y --no-install-recommends \ python-dev \ python-flask \ python-gevent \ + python-gevent-websocket \ python-gflags \ python-h5py \ + python-matplotlib \ python-mock \ python-nose \ python-numpy \ From fd32cde3c22d3e5b8e58e743c4e259e6f6032b29 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 13:27:40 -0700 Subject: [PATCH 14/52] Remove changes to socketio logger --- digits/log.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/digits/log.py b/digits/log.py index 6058d347d..1e24ee4b4 100644 --- a/digits/log.py +++ b/digits/log.py @@ -50,9 +50,6 @@ def process(self, msg, kwargs): return msg, kwargs def setup_logging(): - socketio_logger = logging.getLogger('socketio') - socketio_logger.addHandler(logging.StreamHandler(sys.stdout)) - # Set custom logger logging.setLoggerClass(JobIdLogger) From af888985a5af7eb9f534452e7f4a77196125d8b6 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 14:45:09 -0700 Subject: [PATCH 15/52] Avoid NaN errors and misclassifications in tests --- digits/model/images/classification/test_views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/digits/model/images/classification/test_views.py b/digits/model/images/classification/test_views.py index e61633620..ce444f944 100644 --- a/digits/model/images/classification/test_views.py +++ b/digits/model/images/classification/test_views.py @@ -1112,10 +1112,6 @@ class TestTorchLeNet(BaseTestCreated): IMAGE_WIDTH = 28 IMAGE_HEIGHT = 28 TRAIN_EPOCHS = 20 - # need more aggressive learning rate - # on such a small dataset - LR_POLICY = 'fixed' - LEARNING_RATE = 0.1 # standard lenet model will adjust to color # or grayscale images From 3b2afbdce32ebba3dbccae258b8afb2f5b76e21c Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 16:09:43 -0700 Subject: [PATCH 16/52] Update the ML repo package name --- docs/UbuntuInstall.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/UbuntuInstall.md b/docs/UbuntuInstall.md index c1bd134c1..4e478f920 100644 --- a/docs/UbuntuInstall.md +++ b/docs/UbuntuInstall.md @@ -16,7 +16,7 @@ sudo dpkg -i /tmp/${CUDA_REPO_PKG} rm -f /tmp/${CUDA_REPO_PKG} # Access to Machine Learning packages -ML_REPO_PKG=nvidia-machine-learning-repo_4.0-2_amd64.deb +ML_REPO_PKG=nvidia-machine-learning-repo-ubuntu1404_4.0-2_amd64.deb wget http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1404/x86_64/${ML_REPO_PKG} -O /tmp/${ML_REPO_PKG} sudo dpkg -i /tmp/${ML_REPO_PKG} rm -f /tmp/${ML_REPO_PKG} From b9136e71a2d30f7f70a79300bfd383930845f7c0 Mon Sep 17 00:00:00 2001 From: Luke Yeager Date: Thu, 15 Sep 2016 16:44:24 -0700 Subject: [PATCH 17/52] [Tests] More time for data aug model to converge --- digits/model/images/classification/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/digits/model/images/classification/test_views.py b/digits/model/images/classification/test_views.py index e61633620..e3a49ba93 100644 --- a/digits/model/images/classification/test_views.py +++ b/digits/model/images/classification/test_views.py @@ -1098,6 +1098,7 @@ class TestTorchCreatedCropInForm(BaseTestCreatedCropInForm): class TestTorchCreatedDataAug(BaseTestCreatedDataAug): FRAMEWORK = 'torch' + TRAIN_EPOCHS = 2 class TestTorchCreatedCropInNetwork(BaseTestCreatedCropInNetwork): FRAMEWORK = 'torch' From 8f1e1a688e7a5302cf62f472e6d02dc1ceb011d3 Mon Sep 17 00:00:00 2001 From: Joe Mancewicz Date: Wed, 7 Sep 2016 16:23:46 -0700 Subject: [PATCH 18/52] rearanged group elements and pills on the front page --- digits/templates/home.html | 50 +------------------ digits/templates/layout.html | 4 +- .../partials/home/datasets_pane.html | 31 +++++++++++- .../templates/partials/home/model_pane.html | 31 +++++++++++- .../partials/home/pretrained_model_pane.html | 31 +++++++++++- digits/views.py | 6 +++ 6 files changed, 99 insertions(+), 54 deletions(-) diff --git a/digits/templates/home.html b/digits/templates/home.html index e10a4a45f..7e1becc34 100644 --- a/digits/templates/home.html +++ b/digits/templates/home.html @@ -52,10 +52,6 @@

    Home

    {[jc.model_jobs = (jobs | filter:is_model);'']} {[jc.pretrained_model_jobs = (jobs | filter:is_pretrained_model);'']}
    -
    {[jobs = (jc.running_jobs | filter:search_text | sort_with_empty_at_end:this:jc.storage.show_groups);'']} @@ -209,7 +205,7 @@

    -
    + -
    -
    -
    -
    - New Model - -
    -
    - New Dataset - -
    -
    -
    -
    diff --git a/digits/templates/layout.html b/digits/templates/layout.html index c9e3e8a0e..d0bf09e1b 100644 --- a/digits/templates/layout.html +++ b/digits/templates/layout.html @@ -62,7 +62,7 @@