aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/contrib/image
diff options
context:
space:
mode:
authorGravatar A. Unique TensorFlower <gardener@tensorflow.org>2017-09-15 15:28:15 -0700
committerGravatar TensorFlower Gardener <gardener@tensorflow.org>2017-09-15 15:31:39 -0700
commitbc68dc843f43e6afd9ef2ba207cfa3f0f4a9db4e (patch)
tree5bcef503755f375922489d9c32d1a9b9cf85caa0 /tensorflow/contrib/image
parent30868ef86771acf8632bd8991b65f47e5ce3756e (diff)
Add ops that perform color transforms (including changing value, saturation and hue) in YIQ space.
PiperOrigin-RevId: 168897736
Diffstat (limited to 'tensorflow/contrib/image')
-rwxr-xr-xtensorflow/contrib/image/BUILD75
-rwxr-xr-xtensorflow/contrib/image/__init__.py9
-rw-r--r--tensorflow/contrib/image/kernels/adjust_hsv_in_yiq_op.cc172
-rw-r--r--tensorflow/contrib/image/ops/distort_image_ops.cc60
-rw-r--r--tensorflow/contrib/image/python/kernel_tests/distort_image_ops_test.py338
-rw-r--r--tensorflow/contrib/image/python/ops/distort_image_ops.py138
6 files changed, 791 insertions, 1 deletions
diff --git a/tensorflow/contrib/image/BUILD b/tensorflow/contrib/image/BUILD
index a27bec4801..a18f14112e 100755
--- a/tensorflow/contrib/image/BUILD
+++ b/tensorflow/contrib/image/BUILD
@@ -88,6 +88,7 @@ cuda_py_test(
size = "medium",
srcs = ["python/kernel_tests/image_ops_test.py"],
additional_deps = [
+ ":distort_image_py",
":image_py",
":single_image_random_dot_stereograms_py",
"//third_party/py/numpy",
@@ -100,6 +101,80 @@ cuda_py_test(
)
tf_custom_op_library(
+ name = "python/ops/_distort_image_ops.so",
+ srcs = [
+ "kernels/adjust_hsv_in_yiq_op.cc",
+ "ops/distort_image_ops.cc",
+ ],
+ deps = [
+ "@protobuf_archive//:protobuf",
+ ],
+)
+
+tf_gen_op_libs(
+ op_lib_names = ["distort_image_ops"],
+)
+
+tf_gen_op_wrapper_py(
+ name = "distort_image_ops",
+ deps = [":distort_image_ops_op_lib"],
+)
+
+cc_library(
+ name = "distort_image_ops_cc",
+ srcs = [
+ "kernels/adjust_hsv_in_yiq_op.cc",
+ ],
+ deps = [
+ "//tensorflow/core:framework",
+ "//tensorflow/core:lib",
+ "//third_party/eigen3",
+ ],
+ alwayslink = 1,
+)
+
+py_library(
+ name = "distort_image_py",
+ srcs = [
+ "__init__.py",
+ "python/ops/distort_image_ops.py",
+ ],
+ data = [":python/ops/_distort_image_ops.so"],
+ srcs_version = "PY2AND3",
+ deps = [
+ ":distort_image_ops",
+ "//tensorflow/contrib/util:util_py",
+ "//tensorflow/python:framework",
+ "//tensorflow/python:framework_for_generated_wrappers",
+ "//tensorflow/python:image_ops",
+ "//tensorflow/python:platform",
+ "//tensorflow/python:random_ops",
+ ],
+)
+
+cuda_py_test(
+ name = "distort_image_ops_test",
+ size = "medium",
+ srcs = ["python/kernel_tests/distort_image_ops_test.py"],
+ additional_deps = [
+ ":distort_image_py",
+ ":image_py",
+ ":single_image_random_dot_stereograms_py",
+ "//third_party/py/numpy",
+ "//tensorflow/python:client",
+ "//tensorflow/python:client_testlib",
+ "//tensorflow/python:control_flow_ops",
+ "//tensorflow/python:framework_for_generated_wrappers",
+ "//tensorflow/python:framework_test_lib",
+ "//tensorflow/python:math_ops",
+ "//tensorflow/python:platform_test",
+ "//tensorflow/python:random_ops",
+ "//tensorflow/python:variables",
+ "//tensorflow/core:protos_all_py",
+ ],
+)
+
+tf_custom_op_library(
name = "python/ops/_single_image_random_dot_stereograms.so",
srcs = [
"kernels/single_image_random_dot_stereograms_ops.cc",
diff --git a/tensorflow/contrib/image/__init__.py b/tensorflow/contrib/image/__init__.py
index 1ed19265b3..59a322d3ca 100755
--- a/tensorflow/contrib/image/__init__.py
+++ b/tensorflow/contrib/image/__init__.py
@@ -16,11 +16,14 @@
### API
-This module provides functions for image manipulation; currently, only
+This module provides functions for image manipulation; currently, chrominance
+transformas (including changing saturation and hue) in YIQ space and
projective transforms (including rotation) are supported.
@@angles_to_projective_transforms
@@compose_transforms
+@@adjust_yiq_hsv
+@@random_yiq_hsv
@@rotate
@@transform
@@bipartite_match
@@ -31,6 +34,9 @@ from __future__ import division
from __future__ import print_function
# pylint: disable=line-too-long
+from tensorflow.contrib.image.python.ops.distort_image_ops import adjust_hsv_in_yiq
+from tensorflow.contrib.image.python.ops.distort_image_ops import random_hsv_in_yiq
+
from tensorflow.contrib.image.python.ops.image_ops import angles_to_projective_transforms
from tensorflow.contrib.image.python.ops.image_ops import compose_transforms
from tensorflow.contrib.image.python.ops.image_ops import rotate
@@ -39,5 +45,6 @@ from tensorflow.contrib.image.python.ops.single_image_random_dot_stereograms imp
from tensorflow.python.util.all_util import remove_undocumented
+# pylint: enable=line-too-long
remove_undocumented(__name__)
diff --git a/tensorflow/contrib/image/kernels/adjust_hsv_in_yiq_op.cc b/tensorflow/contrib/image/kernels/adjust_hsv_in_yiq_op.cc
new file mode 100644
index 0000000000..f4962ed69d
--- /dev/null
+++ b/tensorflow/contrib/image/kernels/adjust_hsv_in_yiq_op.cc
@@ -0,0 +1,172 @@
+/* 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.
+==============================================================================*/
+#include <cmath>
+#include <memory>
+#include "third_party/eigen3/unsupported/Eigen/CXX11/Tensor"
+#include "tensorflow/core/framework/op_kernel.h"
+#include "tensorflow/core/framework/register_types.h"
+#include "tensorflow/core/framework/tensor.h"
+#include "tensorflow/core/framework/tensor_shape.h"
+#include "tensorflow/core/framework/types.h"
+#include "tensorflow/core/lib/core/status.h"
+#include "tensorflow/core/platform/logging.h"
+#include "tensorflow/core/util/work_sharder.h"
+
+namespace tensorflow {
+
+typedef Eigen::ThreadPoolDevice CPUDevice;
+typedef Eigen::GpuDevice GPUDevice;
+
+class AdjustHsvInYiqOpBase : public OpKernel {
+ protected:
+ explicit AdjustHsvInYiqOpBase(OpKernelConstruction* context)
+ : OpKernel(context) {}
+
+ struct ComputeOptions {
+ const Tensor* input = nullptr;
+ const Tensor* delta_h = nullptr;
+ const Tensor* scale_s = nullptr;
+ const Tensor* scale_v = nullptr;
+ Tensor* output = nullptr;
+ int64 channel_count = 0;
+ };
+
+ virtual void DoCompute(OpKernelContext* context,
+ const ComputeOptions& options) = 0;
+
+ void Compute(OpKernelContext* context) override {
+ const Tensor& input = context->input(0);
+ const Tensor& delta_h = context->input(1);
+ const Tensor& scale_s = context->input(2);
+ const Tensor& scale_v = context->input(3);
+ OP_REQUIRES(context, input.dims() >= 3,
+ errors::InvalidArgument("input must be at least 3-D, got shape",
+ input.shape().DebugString()));
+ OP_REQUIRES(context, TensorShapeUtils::IsScalar(delta_h.shape()),
+ errors::InvalidArgument("delta_h must be scalar: ",
+ delta_h.shape().DebugString()));
+ OP_REQUIRES(context, TensorShapeUtils::IsScalar(scale_s.shape()),
+ errors::InvalidArgument("scale_s must be scalar: ",
+ scale_s.shape().DebugString()));
+ OP_REQUIRES(context, TensorShapeUtils::IsScalar(scale_v.shape()),
+ errors::InvalidArgument("scale_v must be scalar: ",
+ scale_v.shape().DebugString()));
+ auto channels = input.dim_size(input.dims() - 1);
+ OP_REQUIRES(
+ context, channels == 3,
+ errors::InvalidArgument("input must have 3 channels but instead has ",
+ channels, " channels."));
+
+ Tensor* output = nullptr;
+ OP_REQUIRES_OK(context,
+ context->allocate_output(0, input.shape(), &output));
+
+ if (input.NumElements() > 0) {
+ const int64 channel_count = input.NumElements() / channels;
+ ComputeOptions options;
+ options.input = &input;
+ options.delta_h = &delta_h;
+ options.scale_s = &scale_s;
+ options.scale_v = &scale_v;
+ options.output = output;
+ options.channel_count = channel_count;
+ DoCompute(context, options);
+ }
+ }
+};
+
+template <class Device>
+class AdjustHsvInYiqOp;
+
+template <>
+class AdjustHsvInYiqOp<CPUDevice> : public AdjustHsvInYiqOpBase {
+ public:
+ explicit AdjustHsvInYiqOp(OpKernelConstruction* context)
+ : AdjustHsvInYiqOpBase(context) {}
+
+ void DoCompute(OpKernelContext* context,
+ const ComputeOptions& options) override {
+ const Tensor* input = options.input;
+ Tensor* output = options.output;
+ const int64 channel_count = options.channel_count;
+ static const int kChannelSize = 3;
+ auto input_data = input->shaped<float, 2>({channel_count, kChannelSize});
+ const float delta_h = options.delta_h->scalar<float>()();
+ const float scale_s = options.scale_s->scalar<float>()();
+ const float scale_v = options.scale_v->scalar<float>()();
+ auto output_data = output->shaped<float, 2>({channel_count, kChannelSize});
+ const int kCostPerChannel = 10;
+ const DeviceBase::CpuWorkerThreads& worker_threads =
+ *context->device()->tensorflow_cpu_worker_threads();
+ Shard(worker_threads.num_threads, worker_threads.workers, channel_count,
+ kCostPerChannel,
+ [channel_count, &input_data, &output_data, delta_h, scale_s, scale_v](
+ int64 start_channel, int64 end_channel) {
+ // Using approximate linear transfomation described in:
+ // https://beesbuzz.biz/code/hsv_color_transforms.php
+ /** Get the constants from sympy
+ from sympy import Matrix
+ from sympy.abc import u, w
+ # Projection matrix to YIQ. http://en.wikipedia.org/wiki/YIQ
+ tyiq = Matrix([[0.299, 0.587, 0.114],
+ [0.596, -0.274, -0.322],
+ [0.211, -0.523, 0.312]])
+ # Hue rotation matrix in YIQ space.
+ hue_proj = Matrix(3,3, [v, 0, 0, 0, vsu, -vsw, 0, vsw, vsu])
+ m = tyiq.inv() * hue_proj * tyiq
+ **/
+ // TODO(huangyp): directly compute the projection matrix from tyiq.
+ static const float t[kChannelSize][kChannelSize][kChannelSize] = {
+ {{.299, .701, .16862179492229},
+ {.587, -.587, .329804745287403},
+ {.114, -.114, -0.498426540209694}},
+ {{.299, -.299, -.327963394172371},
+ {.587, .413, .0346106879248821},
+ {.114, -.114, .293352706247489}},
+ {{.299, -.299, 1.24646136576682},
+ {.587, -.587, -1.04322888291964},
+ {.114, .886, -.203232482847173}}};
+ float m[kChannelSize][kChannelSize] = {{0.}};
+ float su = scale_s * std::cos(delta_h);
+ float sw = scale_s * std::sin(delta_h);
+ for (int q_index = 0; q_index < kChannelSize; q_index++) {
+ for (int p_index = 0; p_index < kChannelSize; p_index++) {
+ m[q_index][p_index] = scale_v * (t[q_index][p_index][0] +
+ t[q_index][p_index][1] * su +
+ t[q_index][p_index][2] * sw);
+ }
+ }
+ // Applying projection matrix to input RGB vectors.
+ const float* p = input_data.data() + start_channel * kChannelSize;
+ float* q = output_data.data() + start_channel * kChannelSize;
+ for (int i = start_channel; i < end_channel; i++) {
+ for (int q_index = 0; q_index < kChannelSize; q_index++) {
+ q[q_index] = 0;
+ for (int p_index = 0; p_index < kChannelSize; p_index++) {
+ q[q_index] += m[q_index][p_index] * p[p_index];
+ }
+ }
+ p += kChannelSize;
+ q += kChannelSize;
+ }
+ });
+ }
+};
+
+REGISTER_KERNEL_BUILDER(Name("AdjustHsvInYiq").Device(DEVICE_CPU),
+ AdjustHsvInYiqOp<CPUDevice>);
+
+// TODO(huangyp): add the GPU kernel
+} // namespace tensorflow
diff --git a/tensorflow/contrib/image/ops/distort_image_ops.cc b/tensorflow/contrib/image/ops/distort_image_ops.cc
new file mode 100644
index 0000000000..b169b0b2b2
--- /dev/null
+++ b/tensorflow/contrib/image/ops/distort_image_ops.cc
@@ -0,0 +1,60 @@
+/* Copyright 2016 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.
+==============================================================================*/
+
+#include "tensorflow/core/framework/common_shape_fns.h"
+#include "tensorflow/core/framework/op.h"
+#include "tensorflow/core/framework/shape_inference.h"
+
+namespace tensorflow {
+
+using shape_inference::InferenceContext;
+
+// --------------------------------------------------------------------------
+REGISTER_OP("AdjustHsvInYiq")
+ .Input("images: T")
+ .Input("delta_h: float")
+ .Input("scale_s: float")
+ .Input("scale_v: float")
+ .Output("output: T")
+ .Attr("T: {uint8, int8, int16, int32, int64, half, float, double}")
+ .SetShapeFn([](InferenceContext* c) {
+ return shape_inference::UnchangedShapeWithRankAtLeast(c, 3);
+ })
+ .Doc(R"Doc(
+Adjust the YIQ hue of one or more images.
+
+`images` is a tensor of at least 3 dimensions. The last dimension is
+interpretted as channels, and must be three.
+
+We used linear transfomation described in:
+ beesbuzz.biz/code/hsv_color_transforms.php
+The input image is considered in the RGB colorspace. Conceptually, the RGB
+colors are first mapped into YIQ space, rotated around the Y channel by
+delta_h in radians, multiplying the chrominance channels (I, Q) by scale_s,
+multiplying all channels (Y, I, Q) by scale_v, and then remapped back to RGB
+colorspace. Each operation described above is a linear transformation.
+
+images: Images to adjust. At least 3-D.
+delta_h: A float scale that represents the hue rotation amount, in radians.
+ Although delta_h can be any float value.
+scale_s: A float scale that represents the factor to multiply the saturation by.
+ scale_s needs to be non-negative.
+scale_v: A float scale that represents the factor to multiply the value by.
+ scale_v needs to be non-negative.
+output: The hsv-adjusted image or images. No clipping will be done in this op.
+ The client can clip them using additional ops in their graph.
+)Doc");
+
+} // namespace tensorflow
diff --git a/tensorflow/contrib/image/python/kernel_tests/distort_image_ops_test.py b/tensorflow/contrib/image/python/kernel_tests/distort_image_ops_test.py
new file mode 100644
index 0000000000..b85f19d29b
--- /dev/null
+++ b/tensorflow/contrib/image/python/kernel_tests/distort_image_ops_test.py
@@ -0,0 +1,338 @@
+# 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 for python distort_image_ops."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import time
+
+import numpy as np
+from six.moves import xrange # pylint: disable=redefined-builtin
+
+from tensorflow.contrib.image.python.ops import distort_image_ops
+from tensorflow.core.protobuf import config_pb2
+from tensorflow.python.client import session
+from tensorflow.python.framework import constant_op
+from tensorflow.python.framework import dtypes
+from tensorflow.python.framework import ops
+from tensorflow.python.framework import test_util
+from tensorflow.python.ops import control_flow_ops
+from tensorflow.python.ops import random_ops
+from tensorflow.python.ops import variables
+from tensorflow.python.platform import googletest
+from tensorflow.python.platform import test
+
+
+# TODO(huangyp): also measure the differences between AdjustHsvInYiq and
+# AdjustHsv in core.
+class AdjustHueInYiqTest(test_util.TensorFlowTestCase):
+
+ def _adjust_hue_in_yiq_np(self, x_np, delta_h):
+ """Rotate hue in YIQ space.
+
+ Mathematically we first convert rgb color to yiq space, rotate the hue
+ degrees, and then convert back to rgb.
+
+ Args:
+ x_np: input x with last dimension = 3.
+ delta_h: degree of hue rotation, in radians.
+
+ Returns:
+ Adjusted y with the same shape as x_np.
+ """
+ self.assertEqual(x_np.shape[-1], 3)
+ x_v = x_np.reshape([-1, 3])
+ y_v = np.ndarray(x_v.shape, dtype=x_v.dtype)
+ u = np.cos(delta_h)
+ w = np.sin(delta_h)
+ # Projection matrix from RGB to YIQ. Numbers from wikipedia
+ # https://en.wikipedia.org/wiki/YIQ
+ tyiq = np.array([[0.299, 0.587, 0.114], [0.596, -0.274, -0.322],
+ [0.211, -0.523, 0.312]])
+ y_v = np.dot(x_v, tyiq.T)
+ # Hue rotation matrix in YIQ space.
+ hue_rotation = np.array([[1.0, 0.0, 0.0], [0.0, u, -w], [0.0, w, u]])
+ y_v = np.dot(y_v, hue_rotation.T)
+ # Projecting back to RGB space.
+ y_v = np.dot(y_v, np.linalg.inv(tyiq).T)
+ return y_v.reshape(x_np.shape)
+
+ def _adjust_hue_in_yiq_tf(self, x_np, delta_h):
+ with self.test_session(use_gpu=True):
+ x = constant_op.constant(x_np)
+ y = distort_image_ops.adjust_hsv_in_yiq(x, delta_h, 1, 1)
+ y_tf = y.eval()
+ return y_tf
+
+ def test_adjust_random_hue_in_yiq(self):
+ x_shapes = [
+ [2, 2, 3],
+ [4, 2, 3],
+ [2, 4, 3],
+ [2, 5, 3],
+ [1000, 1, 3],
+ ]
+ test_styles = [
+ 'all_random',
+ 'rg_same',
+ 'rb_same',
+ 'gb_same',
+ 'rgb_same',
+ ]
+ for x_shape in x_shapes:
+ for test_style in test_styles:
+ x_np = np.random.rand(*x_shape) * 255.
+ delta_h = (np.random.rand() * 2.0 - 1.0) * np.pi
+ if test_style == 'all_random':
+ pass
+ elif test_style == 'rg_same':
+ x_np[..., 1] = x_np[..., 0]
+ elif test_style == 'rb_same':
+ x_np[..., 2] = x_np[..., 0]
+ elif test_style == 'gb_same':
+ x_np[..., 2] = x_np[..., 1]
+ elif test_style == 'rgb_same':
+ x_np[..., 1] = x_np[..., 0]
+ x_np[..., 2] = x_np[..., 0]
+ else:
+ raise AssertionError('Invalid test style: %s' % (test_style))
+ y_np = self._adjust_hue_in_yiq_np(x_np, delta_h)
+ y_tf = self._adjust_hue_in_yiq_tf(x_np, delta_h)
+ self.assertAllClose(y_tf, y_np, rtol=2e-4, atol=1e-4)
+
+ def test_invalid_shapes(self):
+ x_np = np.random.rand(2, 3) * 255.
+ delta_h = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesRegexp(ValueError, 'Shape must be at least rank 3'):
+ self._adjust_hue_in_yiq_tf(x_np, delta_h)
+ x_np = np.random.rand(4, 2, 4) * 255.
+ delta_h = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesOpError('input must have 3 channels but instead has '
+ '4 channels'):
+ self._adjust_hue_in_yiq_tf(x_np, delta_h)
+
+
+class AdjustValueInYiqTest(test_util.TensorFlowTestCase):
+
+ def _adjust_value_in_yiq_np(self, x_np, scale):
+ return x_np * scale
+
+ def _adjust_value_in_yiq_tf(self, x_np, scale):
+ with self.test_session(use_gpu=True):
+ x = constant_op.constant(x_np)
+ y = distort_image_ops.adjust_hsv_in_yiq(x, 0, 1, scale)
+ y_tf = y.eval()
+ return y_tf
+
+ def test_adjust_random_value_in_yiq(self):
+ x_shapes = [
+ [2, 2, 3],
+ [4, 2, 3],
+ [2, 4, 3],
+ [2, 5, 3],
+ [1000, 1, 3],
+ ]
+ test_styles = [
+ 'all_random',
+ 'rg_same',
+ 'rb_same',
+ 'gb_same',
+ 'rgb_same',
+ ]
+ for x_shape in x_shapes:
+ for test_style in test_styles:
+ x_np = np.random.rand(*x_shape) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ if test_style == 'all_random':
+ pass
+ elif test_style == 'rg_same':
+ x_np[..., 1] = x_np[..., 0]
+ elif test_style == 'rb_same':
+ x_np[..., 2] = x_np[..., 0]
+ elif test_style == 'gb_same':
+ x_np[..., 2] = x_np[..., 1]
+ elif test_style == 'rgb_same':
+ x_np[..., 1] = x_np[..., 0]
+ x_np[..., 2] = x_np[..., 0]
+ else:
+ raise AssertionError('Invalid test style: %s' % (test_style))
+ y_np = self._adjust_value_in_yiq_np(x_np, scale)
+ y_tf = self._adjust_value_in_yiq_tf(x_np, scale)
+ self.assertAllClose(y_tf, y_np, rtol=2e-5, atol=1e-5)
+
+ def test_invalid_shapes(self):
+ x_np = np.random.rand(2, 3) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesRegexp(ValueError, 'Shape must be at least rank 3'):
+ self._adjust_value_in_yiq_tf(x_np, scale)
+ x_np = np.random.rand(4, 2, 4) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesOpError('input must have 3 channels but instead has '
+ '4 channels'):
+ self._adjust_value_in_yiq_tf(x_np, scale)
+
+
+class AdjustSaturationInYiqTest(test_util.TensorFlowTestCase):
+
+ def _adjust_saturation_in_yiq_tf(self, x_np, scale):
+ with self.test_session(use_gpu=True):
+ x = constant_op.constant(x_np)
+ y = distort_image_ops.adjust_hsv_in_yiq(x, 0, scale, 1)
+ y_tf = y.eval()
+ return y_tf
+
+ def _adjust_saturation_in_yiq_np(self, x_np, scale):
+ """Adjust saturation using linear interpolation."""
+ rgb_weights = np.array([0.299, 0.587, 0.114])
+ gray = np.sum(x_np * rgb_weights, axis=-1, keepdims=True)
+ y_v = x_np * scale + gray * (1 - scale)
+ return y_v
+
+ def test_adjust_random_saturation_in_yiq(self):
+ x_shapes = [
+ [2, 2, 3],
+ [4, 2, 3],
+ [2, 4, 3],
+ [2, 5, 3],
+ [1000, 1, 3],
+ ]
+ test_styles = [
+ 'all_random',
+ 'rg_same',
+ 'rb_same',
+ 'gb_same',
+ 'rgb_same',
+ ]
+ with self.test_session():
+ for x_shape in x_shapes:
+ for test_style in test_styles:
+ x_np = np.random.rand(*x_shape) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ if test_style == 'all_random':
+ pass
+ elif test_style == 'rg_same':
+ x_np[..., 1] = x_np[..., 0]
+ elif test_style == 'rb_same':
+ x_np[..., 2] = x_np[..., 0]
+ elif test_style == 'gb_same':
+ x_np[..., 2] = x_np[..., 1]
+ elif test_style == 'rgb_same':
+ x_np[..., 1] = x_np[..., 0]
+ x_np[..., 2] = x_np[..., 0]
+ else:
+ raise AssertionError('Invalid test style: %s' % (test_style))
+ y_baseline = self._adjust_saturation_in_yiq_np(x_np, scale)
+ y_tf = self._adjust_saturation_in_yiq_tf(x_np, scale)
+ self.assertAllClose(y_tf, y_baseline, rtol=2e-5, atol=1e-5)
+
+ def test_invalid_shapes(self):
+ x_np = np.random.rand(2, 3) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesRegexp(ValueError, 'Shape must be at least rank 3'):
+ self._adjust_saturation_in_yiq_tf(x_np, scale)
+ x_np = np.random.rand(4, 2, 4) * 255.
+ scale = np.random.rand() * 2.0 - 1.0
+ with self.assertRaisesOpError('input must have 3 channels but instead has '
+ '4 channels'):
+ self._adjust_saturation_in_yiq_tf(x_np, scale)
+
+
+class AdjustHueInYiqBenchmark(test.Benchmark):
+
+ def _benchmark_adjust_hue_in_yiq(self, device, cpu_count):
+ image_shape = [299, 299, 3]
+ warmup_rounds = 100
+ benchmark_rounds = 1000
+ config = config_pb2.ConfigProto()
+ if cpu_count is not None:
+ config.inter_op_parallelism_threads = 1
+ config.intra_op_parallelism_threads = cpu_count
+ with session.Session('', graph=ops.Graph(), config=config) as sess:
+ with ops.device(device):
+ inputs = variables.Variable(
+ random_ops.random_uniform(image_shape, dtype=dtypes.float32) * 255,
+ trainable=False,
+ dtype=dtypes.float32)
+ delta = constant_op.constant(0.1, dtype=dtypes.float32)
+ outputs = distort_image_ops.adjust_hsv_in_yiq(inputs, delta, 1, 1)
+ run_op = control_flow_ops.group(outputs)
+ sess.run(variables.global_variables_initializer())
+ for i in xrange(warmup_rounds + benchmark_rounds):
+ if i == warmup_rounds:
+ start = time.time()
+ sess.run(run_op)
+ end = time.time()
+ step_time = (end - start) / benchmark_rounds
+ tag = device + '_%s' % (cpu_count if cpu_count is not None else 'all')
+ print('benchmarkadjust_hue_in_yiq_299_299_3_%s step_time: %.2f us' %
+ (tag, step_time * 1e6))
+ self.report_benchmark(
+ name='benchmarkadjust_hue_in_yiq_299_299_3_%s' % (tag),
+ iters=benchmark_rounds,
+ wall_time=step_time)
+
+ def benchmark_adjust_hue_in_yiqCpu1(self):
+ self._benchmark_adjust_hue_in_yiq('/cpu:0', 1)
+
+ def benchmark_adjust_hue_in_yiqCpuAll(self):
+ self._benchmark_adjust_hue_in_yiq('/cpu:0', None)
+
+
+class AdjustSaturationInYiqBenchmark(test.Benchmark):
+
+ def _benchmark_adjust_saturation_in_yiq(self, device, cpu_count):
+ image_shape = [299, 299, 3]
+ warmup_rounds = 100
+ benchmark_rounds = 1000
+ config = config_pb2.ConfigProto()
+ if cpu_count is not None:
+ config.inter_op_parallelism_threads = 1
+ config.intra_op_parallelism_threads = cpu_count
+ with session.Session('', graph=ops.Graph(), config=config) as sess:
+ with ops.device(device):
+ inputs = variables.Variable(
+ random_ops.random_uniform(image_shape, dtype=dtypes.float32) * 255,
+ trainable=False,
+ dtype=dtypes.float32)
+ scale = constant_op.constant(0.1, dtype=dtypes.float32)
+ outputs = distort_image_ops.adjust_hsv_in_yiq(inputs, 0, scale, 1)
+ run_op = control_flow_ops.group(outputs)
+ sess.run(variables.global_variables_initializer())
+ for _ in xrange(warmup_rounds):
+ sess.run(run_op)
+ start = time.time()
+ for _ in xrange(benchmark_rounds):
+ sess.run(run_op)
+ end = time.time()
+ step_time = (end - start) / benchmark_rounds
+ tag = '%s' % (cpu_count) if cpu_count is not None else '_all'
+ print('benchmarkAdjustSaturationInYiq_299_299_3_cpu%s step_time: %.2f us' %
+ (tag, step_time * 1e6))
+ self.report_benchmark(
+ name='benchmarkAdjustSaturationInYiq_299_299_3_cpu%s' % (tag),
+ iters=benchmark_rounds,
+ wall_time=step_time)
+
+ def benchmark_adjust_saturation_in_yiq_cpu1(self):
+ self._benchmark_adjust_saturation_in_yiq('/cpu:0', 1)
+
+ def benchmark_adjust_saturation_in_yiq_cpu_all(self):
+ self._benchmark_adjust_saturation_in_yiq('/cpu:0', None)
+
+
+if __name__ == '__main__':
+ googletest.main()
diff --git a/tensorflow/contrib/image/python/ops/distort_image_ops.py b/tensorflow/contrib/image/python/ops/distort_image_ops.py
new file mode 100644
index 0000000000..39f023a2b4
--- /dev/null
+++ b/tensorflow/contrib/image/python/ops/distort_image_ops.py
@@ -0,0 +1,138 @@
+# 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.
+# ==============================================================================
+"""Python layer for distort_image_ops."""
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from tensorflow.contrib.util import loader
+from tensorflow.python.framework import dtypes
+from tensorflow.python.framework import ops
+from tensorflow.python.ops import image_ops
+from tensorflow.python.ops import random_ops
+from tensorflow.python.platform import resource_loader
+
+_distort_image_ops = loader.load_op_library(
+ resource_loader.get_path_to_datafile('_distort_image_ops.so'))
+
+
+# pylint: disable=invalid-name
+def random_hsv_in_yiq(image,
+ max_delta_hue=0,
+ lower_saturation=1,
+ upper_saturation=1,
+ lower_value=1,
+ upper_value=1,
+ seed=None):
+ """Adjust hue, saturation, value of an RGB image randomly in YIQ color space.
+
+ Equivalent to `adjust_yiq_hsv()` but uses a `delta_h` randomly
+ picked in the interval `[-max_delta_hue, max_delta_hue]`, a `scale_saturation`
+ randomly picked in the interval `[lower_saturation, upper_saturation]`, and
+ a `scale_value` randomly picked in the interval
+ `[lower_saturation, upper_saturation]`.
+
+ Args:
+ image: RGB image or images. Size of the last dimension must be 3.
+ max_delta_hue: float. Maximum value for the random delta_hue. Passing 0
+ disables adjusting hue.
+ lower_saturation: float. Lower bound for the random scale_saturation.
+ upper_saturation: float. Upper bound for the random scale_saturation.
+ lower_value: float. Lower bound for the random scale_value.
+ upper_value: float. Upper bound for the random scale_value.
+ seed: An operation-specific seed. It will be used in conjunction
+ with the graph-level seed to determine the real seeds that will be
+ used in this operation. Please see the documentation of
+ set_random_seed for its interaction with the graph-level random seed.
+
+ Returns:
+ 3-D float tensor of shape `[height, width, channels]`.
+
+ Raises:
+ ValueError: if `max_delta`, `lower_saturation`, `upper_saturation`,
+ `lower_value`, or `upper_Value` is invalid.
+ """
+ if max_delta_hue < 0:
+ raise ValueError('max_delta must be non-negative.')
+
+ if lower_saturation < 0:
+ raise ValueError('lower_saturation must be non-negative.')
+
+ if lower_value < 0:
+ raise ValueError('lower_value must be non-negative.')
+
+ if lower_saturation > upper_saturation:
+ raise ValueError('lower_saturation must be < upper_saturation.')
+
+ if lower_value > upper_value:
+ raise ValueError('lower_value must be < upper_value.')
+
+ if max_delta_hue == 0:
+ delta_hue = 0
+ else:
+ delta_hue = random_ops.random_uniform(
+ [], -max_delta_hue, max_delta_hue, seed=seed)
+ if lower_saturation == upper_saturation:
+ scale_saturation = lower_saturation
+ else:
+ scale_saturation = random_ops.random_uniform(
+ [], lower_saturation, upper_saturation, seed=seed)
+ if lower_value == upper_value:
+ scale_value = lower_value
+ else:
+ scale_value = random_ops.random_uniform(
+ [], lower_value, upper_value, seed=seed)
+ return adjust_hsv_in_yiq(image, delta_hue, scale_saturation, scale_value)
+
+
+def adjust_hsv_in_yiq(image,
+ delta_hue=0,
+ scale_saturation=1,
+ scale_value=1,
+ name=None):
+ """Adjust hue, saturation, value of an RGB image in YIQ color space.
+
+ This is a convenience method that converts an RGB image to float
+ representation, converts it to YIQ, rotates the color around the Y channel by
+ delta_hue in radians, scales the chrominance channels (I, Q) by
+ scale_saturation, scales all channels (Y, I, Q) by scale_value,
+ converts back to RGB, and then back to the original data type.
+
+ `image` is an RGB image. The image hue is adjusted by converting the
+ image to YIQ, rotating around the luminance channel (Y) by
+ `delta_hue` in radians, multiplying the chrominance channels (I, Q) by
+ `scale_saturation`, and multiplying all channels (Y, I, Q) by
+ `scale_value`. The image is then converted back to RGB.
+
+ Args:
+ image: RGB image or images. Size of the last dimension must be 3.
+ delta_hue: float, the hue rotation amount, in radians.
+ scale_saturation: float, factor to multiply the saturation by.
+ scale_value: float, factor to multiply the value by.
+ name: A name for this operation (optional).
+
+ Returns:
+ Adjusted image(s), same shape and DType as `image`.
+ """
+ with ops.name_scope(name, 'adjust_hsv_in_yiq', [image]) as name:
+ image = ops.convert_to_tensor(image, name='image')
+ # Remember original dtype to so we can convert back if needed
+ orig_dtype = image.dtype
+ flt_image = image_ops.convert_image_dtype(image, dtypes.float32)
+
+ rgb_altered = _distort_image_ops.adjust_hsv_in_yiq(
+ flt_image, delta_hue, scale_saturation, scale_value)
+
+ return image_ops.convert_image_dtype(rgb_altered, orig_dtype)