aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--tensorflow/BUILD1
-rwxr-xr-xtensorflow/contrib/cmake/tf_python.cmake1
-rw-r--r--tensorflow/tensorboard/BUILD1
-rw-r--r--tensorflow/tensorboard/plugins/images/BUILD53
-rw-r--r--tensorflow/tensorboard/plugins/images/images_plugin.py150
-rw-r--r--tensorflow/tensorboard/plugins/images/images_plugin_test.py164
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()