diff options
-rw-r--r-- | tensorflow/BUILD | 1 | ||||
-rwxr-xr-x | tensorflow/contrib/cmake/tf_python.cmake | 1 | ||||
-rw-r--r-- | tensorflow/tensorboard/BUILD | 1 | ||||
-rw-r--r-- | tensorflow/tensorboard/plugins/images/BUILD | 53 | ||||
-rw-r--r-- | tensorflow/tensorboard/plugins/images/images_plugin.py | 150 | ||||
-rw-r--r-- | tensorflow/tensorboard/plugins/images/images_plugin_test.py | 164 |
6 files changed, 370 insertions, 0 deletions
diff --git a/tensorflow/BUILD b/tensorflow/BUILD index ffb1b1036d..169fe32cb4 100644 --- a/tensorflow/BUILD +++ b/tensorflow/BUILD @@ -378,6 +378,7 @@ filegroup( "//tensorflow/tensorboard/demo:all_files", "//tensorflow/tensorboard/java/org/tensorflow/tensorboard/vulcanize:all_files", "//tensorflow/tensorboard/plugins:all_files", + "//tensorflow/tensorboard/plugins/images:all_files", "//tensorflow/tensorboard/plugins/projector:all_files", "//tensorflow/tensorboard/plugins/scalars:all_files", "//tensorflow/tensorboard/plugins/text:all_files", diff --git a/tensorflow/contrib/cmake/tf_python.cmake b/tensorflow/contrib/cmake/tf_python.cmake index e08ee05592..d41ccff02f 100755 --- a/tensorflow/contrib/cmake/tf_python.cmake +++ b/tensorflow/contrib/cmake/tf_python.cmake @@ -229,6 +229,7 @@ add_python_module("tensorflow/tensorboard") add_python_module("tensorflow/tensorboard/backend") add_python_module("tensorflow/tensorboard/backend/event_processing") add_python_module("tensorflow/tensorboard/plugins") +add_python_module("tensorflow/tensorboard/plugins/images") add_python_module("tensorflow/tensorboard/plugins/projector") add_python_module("tensorflow/tensorboard/plugins/scalars") add_python_module("tensorflow/tensorboard/plugins/text") diff --git a/tensorflow/tensorboard/BUILD b/tensorflow/tensorboard/BUILD index 7df915d78f..e6ade9b62b 100644 --- a/tensorflow/tensorboard/BUILD +++ b/tensorflow/tensorboard/BUILD @@ -14,6 +14,7 @@ py_binary( "//tensorflow/python:platform", "//tensorflow/tensorboard/backend:application", "//tensorflow/tensorboard/backend/event_processing:event_file_inspector", + "//tensorflow/tensorboard/plugins/images:images_plugin", "//tensorflow/tensorboard/plugins/projector:projector_plugin", "//tensorflow/tensorboard/plugins/scalars:scalars_plugin", "//tensorflow/tensorboard/plugins/text:text_plugin", diff --git a/tensorflow/tensorboard/plugins/images/BUILD b/tensorflow/tensorboard/plugins/images/BUILD new file mode 100644 index 0000000000..abb284481d --- /dev/null +++ b/tensorflow/tensorboard/plugins/images/BUILD @@ -0,0 +1,53 @@ +# Description: +# TensorBoard plugin for images + +package(default_visibility = ["//tensorflow:internal"]) + +licenses(["notice"]) # Apache 2.0 + +exports_files(["LICENSE"]) + +load("//tensorflow:tensorflow.bzl", "py_test") + +py_library( + name = "images_plugin", + srcs = ["images_plugin.py"], + srcs_version = "PY2AND3", + visibility = [ + "//tensorflow:internal", + ], + deps = [ + "//tensorflow/python:summary", + "//tensorflow/tensorboard/backend:http_util", + "//tensorflow/tensorboard/backend/event_processing:event_accumulator", + "//tensorflow/tensorboard/plugins:base_plugin", + "@org_pocoo_werkzeug//:werkzeug", + "@six_archive//:six", + ], +) + +py_test( + name = "images_plugin_test", + size = "small", + srcs = ["images_plugin_test.py"], + srcs_version = "PY2AND3", + deps = [ + ":images_plugin", + "//tensorflow/core:protos_all_py", + "//tensorflow/python:array_ops", + "//tensorflow/python:client", + "//tensorflow/python:client_testlib", + "//tensorflow/python:platform", + "//tensorflow/python:summary", + "//tensorflow/tensorboard/backend:application", + "//tensorflow/tensorboard/backend/event_processing:event_multiplexer", + "@org_pocoo_werkzeug//:werkzeug", + "@six_archive//:six", + ], +) + +filegroup( + name = "all_files", + srcs = glob(["**"]), + visibility = ["//tensorflow:__pkg__"], +) diff --git a/tensorflow/tensorboard/plugins/images/images_plugin.py b/tensorflow/tensorboard/plugins/images/images_plugin.py new file mode 100644 index 0000000000..99704c36af --- /dev/null +++ b/tensorflow/tensorboard/plugins/images/images_plugin.py @@ -0,0 +1,150 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""The TensorBoard Images plugin.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import imghdr + +from six.moves import urllib +from werkzeug import wrappers + +from tensorflow.tensorboard.backend import http_util +from tensorflow.tensorboard.backend.event_processing import event_accumulator +from tensorflow.tensorboard.plugins import base_plugin + +_PLUGIN_PREFIX_ROUTE = event_accumulator.IMAGES + +_IMGHDR_TO_MIMETYPE = { + 'bmp': 'image/bmp', + 'gif': 'image/gif', + 'jpeg': 'image/jpeg', + 'png': 'image/png' +} + +_DEFAULT_IMAGE_MIMETYPE = 'application/octet-stream' + + +class ImagesPlugin(base_plugin.TBPlugin): + """Images Plugin for TensorBoard.""" + + plugin_name = _PLUGIN_PREFIX_ROUTE + + def get_plugin_apps(self, multiplexer, unused_logdir): + self._multiplexer = multiplexer + return { + '/images': self._serve_image_metadata, + '/individualImage': self._serve_individual_image, + '/tags': self._serve_tags, + } + + def is_active(self): + """The images plugin is active iff any run has at least one relevant tag.""" + return any(self.index_impl().values()) + + def _index_impl(self): + return { + run_name: run_data[event_accumulator.IMAGES] + for (run_name, run_data) in self._multiplexer.Runs().items() + if event_accumulator.IMAGES in run_data + } + + @wrappers.Request.application + def _serve_image_metadata(self, request): + """Given a tag and list of runs, serve a list of metadata for images. + + Note that the images themselves are not sent; instead, we respond with URLs + to the images. The frontend should treat these URLs as opaque and should not + try to parse information about them or generate them itself, as the format + may change. + + Args: + request: A werkzeug.wrappers.Request object. + + Returns: + A werkzeug.Response application. + """ + tag = request.args.get('tag') + run = request.args.get('run') + + images = self._multiplexer.Images(run, tag) + response = self._image_response_for_run(images, run, tag) + return http_util.Respond(request, response, 'application/json') + + def _image_response_for_run(self, run_images, run, tag): + """Builds a JSON-serializable object with information about run_images. + + Args: + run_images: A list of event_accumulator.ImageValueEvent objects. + run: The name of the run. + tag: The name of the tag the images all belong to. + + Returns: + A list of dictionaries containing the wall time, step, URL, width, and + height for each image. + """ + response = [] + for index, run_image in enumerate(run_images): + response.append({ + 'wall_time': run_image.wall_time, + 'step': run_image.step, + # We include the size so that the frontend can add that to the <img> + # tag so that the page layout doesn't change when the image loads. + 'width': run_image.width, + 'height': run_image.height, + 'query': self._query_for_individual_image(run, tag, index) + }) + return response + + def _query_for_individual_image(self, run, tag, index): + """Builds a URL for accessing the specified image. + + This should be kept in sync with _serve_image_metadata. Note that the URL is + *not* guaranteed to always return the same image, since images may be + unloaded from the reservoir as new images come in. + + Args: + run: The name of the run. + tag: The tag. + index: The index of the image. Negative values are OK. + + Returns: + A string representation of a URL that will load the index-th sampled image + in the given run with the given tag. + """ + query_string = urllib.parse.urlencode({ + 'run': run, + 'tag': tag, + 'index': index + }) + return query_string + + @wrappers.Request.application + def _serve_individual_image(self, request): + """Serves an individual image.""" + tag = request.args.get('tag') + run = request.args.get('run') + index = int(request.args.get('index')) + image = self._multiplexer.Images(run, tag)[index] + image_type = imghdr.what(None, image.encoded_image_string) + content_type = _IMGHDR_TO_MIMETYPE.get(image_type, _DEFAULT_IMAGE_MIMETYPE) + return http_util.Respond(request, image.encoded_image_string, content_type) + + @wrappers.Request.application + def _serve_tags(self, request): + index = self._index_impl() + return http_util.Respond(request, index, 'application/json') diff --git a/tensorflow/tensorboard/plugins/images/images_plugin_test.py b/tensorflow/tensorboard/plugins/images/images_plugin_test.py new file mode 100644 index 0000000000..6e951a2ca6 --- /dev/null +++ b/tensorflow/tensorboard/plugins/images/images_plugin_test.py @@ -0,0 +1,164 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests the Tensorboard images plugin.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import json +import os +import shutil +import tempfile + +import numpy +from six.moves import urllib +from six.moves import xrange # pylint: disable=redefined-builtin +from werkzeug import test as werkzeug_test +from werkzeug import wrappers + +from tensorflow.python.client import session +from tensorflow.python.framework import dtypes +from tensorflow.python.framework import ops +from tensorflow.python.ops import array_ops +from tensorflow.python.platform import test +from tensorflow.python.summary import summary +from tensorflow.tensorboard.backend import application +from tensorflow.tensorboard.backend.event_processing import event_multiplexer +from tensorflow.tensorboard.plugins.images import images_plugin + + +class ImagesPluginTest(test.TestCase): + + def setUp(self): + self.log_dir = tempfile.mkdtemp() + + # We use numpy.random to generate images. We seed to avoid non-determinism + # in this test. + numpy.random.seed(42) + + # Create image summaries for run foo. + ops.reset_default_graph() + sess = session.Session() + placeholder = array_ops.placeholder(dtypes.uint8) + summary.image(name="baz", tensor=placeholder) + merged_summary_op = summary.merge_all() + foo_directory = os.path.join(self.log_dir, "foo") + writer = summary.FileWriter(foo_directory) + writer.add_graph(sess.graph) + for step in xrange(2): + writer.add_summary(sess.run(merged_summary_op, feed_dict={ + placeholder: (numpy.random.rand(1, 16, 42, 3) * 255).astype( + numpy.uint8) + }), global_step=step) + writer.close() + + # Create image summaries for run bar. + ops.reset_default_graph() + sess = session.Session() + placeholder = array_ops.placeholder(dtypes.uint8) + summary.image(name="quux", tensor=placeholder) + merged_summary_op = summary.merge_all() + bar_directory = os.path.join(self.log_dir, "bar") + writer = summary.FileWriter(bar_directory) + writer.add_graph(sess.graph) + for step in xrange(2): + writer.add_summary(sess.run(merged_summary_op, feed_dict={ + placeholder: (numpy.random.rand(1, 6, 8, 3) * 255).astype( + numpy.uint8) + }), global_step=step) + writer.close() + + # Start a server with the plugin. + multiplexer = event_multiplexer.EventMultiplexer({ + "foo": foo_directory, + "bar": bar_directory, + }) + plugin = images_plugin.ImagesPlugin() + wsgi_app = application.TensorBoardWSGIApp( + self.log_dir, [plugin], multiplexer, reload_interval=0) + self.server = werkzeug_test.Client(wsgi_app, wrappers.BaseResponse) + self.routes = plugin.get_plugin_apps(multiplexer, self.log_dir) + + def tearDown(self): + shutil.rmtree(self.log_dir, ignore_errors=True) + + def _DeserializeResponse(self, byte_content): + """Deserializes byte content that is a JSON encoding. + + Args: + byte_content: The byte content of a response. + + Returns: + The deserialized python object decoded from JSON. + """ + return json.loads(byte_content.decode("utf-8")) + + def testRoutesProvided(self): + """Tests that the plugin offers the correct routes.""" + self.assertIsInstance(self.routes["/images"], collections.Callable) + self.assertIsInstance(self.routes["/individualImage"], collections.Callable) + self.assertIsInstance(self.routes["/tags"], collections.Callable) + + def testImagesRoute(self): + """Tests that the /images routes returns with the correct data.""" + response = self.server.get( + "/data/plugin/images/images?run=foo&tag=baz/image/0") + self.assertEqual(200, response.status_code) + + # Verify that the correct entries are returned. + entries = self._DeserializeResponse(response.get_data()) + self.assertEqual(2, len(entries)) + + # Verify that the 1st entry is correct. + entry = entries[0] + self.assertEqual(42, entry["width"]) + self.assertEqual(16, entry["height"]) + self.assertEqual(0, entry["step"]) + parsed_query = urllib.parse.parse_qs(entry["query"]) + self.assertListEqual(["0"], parsed_query["index"]) + self.assertListEqual(["foo"], parsed_query["run"]) + self.assertListEqual(["baz/image/0"], parsed_query["tag"]) + + # Verify that the 2nd entry is correct. + entry = entries[1] + self.assertEqual(42, entry["width"]) + self.assertEqual(16, entry["height"]) + self.assertEqual(1, entry["step"]) + parsed_query = urllib.parse.parse_qs(entry["query"]) + self.assertListEqual(["1"], parsed_query["index"]) + self.assertListEqual(["foo"], parsed_query["run"]) + self.assertListEqual(["baz/image/0"], parsed_query["tag"]) + + def testIndividualImageRoute(self): + """Tests fetching an individual image.""" + response = self.server.get( + "/data/plugin/images/individualImage?run=bar&tag=quux/image/0&index=0") + self.assertEqual(200, response.status_code) + self.assertEqual("image/png", response.headers.get("content-type")) + + def testRunsRoute(self): + """Tests that the /runs route offers the correct run to tag mapping.""" + response = self.server.get("/data/plugin/images/tags") + self.assertEqual(200, response.status_code) + self.assertDictEqual({ + "foo": ["baz/image/0"], + "bar": ["quux/image/0"] + }, self._DeserializeResponse(response.get_data())) + + +if __name__ == "__main__": + test.main() |