Sitelet https://github.com/tensorflow/tensorflow/pull/126086
Skip to content

Register missing AdjustContrastv2 gradient - #126086

Open
VaggelisGian wants to merge 2 commits into
tensorflow:masterfrom
VaggelisGian:fix-adjust-contrast-gradient
Open

Register missing AdjustContrastv2 gradient#126086
VaggelisGian wants to merge 2 commits into
tensorflow:masterfrom
VaggelisGian:fix-adjust-contrast-gradient

Conversation

@VaggelisGian

Copy link
Copy Markdown

Fixes #126083 (the adjust_contrast part; hue and saturation need piecewise HSV derivations and are left for separate changes)

Summary

tf.image.adjust_contrast could not be used inside a GradientTape: differentiating it raised

LookupError: gradient registry has no entry for: AdjustContrastv2

because the raw op had no Python gradient registration, while its sibling tf.image.adjust_brightness works thanks to a composite wrapper.

The kernel computes (images - mean) * factor + mean, where mean is taken per batch and channel over the last three dimensions, interpreted as [height, width, channels] with any leading dimensions folded into batch. The new registration in tensorflow/python/ops/image_grad.py returns

  • factor * grad + (1 - factor) * mean(grad) over those same axes for the images input,
  • sum(grad * (images - mean)) for the scalar contrast factor,

with the factor reduction formed in float32 so the half-precision path cannot overflow before the upcast.

Testing

New AdjustContrastOpTestBase in tensorflow/python/ops/image_grad_test_base.py (wired into image_grad_test.py) checks the analytical gradient against finite differences of the real forward kernel via gradient_checker_v2, for rank 3, 4 and 5 inputs, which pins down the spatial reduction axes against the actual op rather than an assumption:

$ python tensorflow/python/ops/image_grad_test.py AdjustContrastOpTest
Ran 4 tests in 2.486s

OK (skipped=1)

That run uses a pip tf-nightly build (2.22.0-dev20260825) with the patched image_grad.py overlaid; the repo copy was byte identical to the installed one before patching. With pristine image_grad.py the rank 3 and rank 4 cases fail with the LookupError quoted above.

Lint:

$ pylint --rcfile=tensorflow/tools/ci_build/pylintrc tensorflow/python/ops/image_grad.py tensorflow/python/ops/image_grad_test_base.py tensorflow/python/ops/image_grad_test.py
Your code has been rated at 10.00/10

A RELEASE.md entry under 2.22.0 bug fixes is included. One compatibility note: downstream code that worked around this gap by registering its own AdjustContrastv2 Python gradient will now see a duplicate-registration error at import.

tf.image.adjust_contrast could not be differentiated: the raw
AdjustContrastv2 op had no Python gradient registration, so any tape
through it raised "LookupError: gradient registry has no entry for:
AdjustContrastv2". The kernel computes (images - mean) * factor + mean,
where mean is taken per batch and channel over the last three
dimensions, interpreted as [height, width, channels]. The new
registration reduces the incoming gradient over those same axes and
returns sum(grad * (images - mean)) for the scalar factor. The half
and float paths form the factor reduction in float32 to avoid
half-precision overflow.

Filed as issue 126083; adjust_hue and adjust_saturation need piecewise
HSV derivations and are left for separate changes.

Test Plan:
  Added AdjustContrastOpTestBase with gradient_checker_v2 cases for
  rank 3, 4 and 5 inputs so the analytical gradient is checked against
  finite differences of the real forward kernel. Ran
  image_grad_test.py AdjustContrastOpTest against a pip tf-nightly
  build with the patched image_grad.py overlaid: "Ran 4 tests in
  2.486s / OK (skipped=1)". With pristine image_grad.py the rank 3
  and rank 4 cases fail with the LookupError above.
@google-ml-butler google-ml-butler Bot added the size:M CL Change Size: Medium label Aug 25, 2026
@google-ml-butler
google-ml-butler Bot requested a review from cantonios August 25, 2026 13:42
@google-ml-butler google-ml-butler Bot added the awaiting review Pull request awaiting review label Aug 25, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request registers the missing Python gradient for the AdjustContrastv2 op (tf.image.adjust_contrast) and adds corresponding unit tests. The review feedback identifies two key issues in the gradient implementation: a potential TypeError when the input image rank is unknown at graph construction time, and a potential dtype mismatch error during backpropagation because the gradient for the contrast factor is not cast back to the input factor's original data type.

Comment thread tensorflow/python/ops/image_grad.py Outdated
Comment on lines +68 to +72
rank = images.shape.rank
if rank is not None:
spatial_axes = list(range(rank - 3, rank - 1))
else:
spatial_axes = math_ops.range(rank - 3, rank - 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the input images has an unknown rank at graph construction time (i.e., rank is None), the else branch will attempt to evaluate rank - 3 in Python. This will raise a TypeError: unsupported operand type(s) for -: 'NoneType' and 'int'. To support dynamic/unknown ranks robustly, we should retrieve the rank dynamically using array_ops.rank(images).

Suggested change
rank = images.shape.rank
if rank is not None:
spatial_axes = list(range(rank - 3, rank - 1))
else:
spatial_axes = math_ops.range(rank - 3, rank - 1)
rank = images.shape.rank
if rank is not None:
spatial_axes = list(range(rank - 3, rank - 1))
else:
num_dims = array_ops.rank(images)
spatial_axes = math_ops.range(num_dims - 3, num_dims - 1)
References
  1. Ensure proper error handling and defensive checks; flag fragile or environment-dependent logic. (link)

Comment on lines +79 to +82
grad_factor = math_ops.reduce_sum(
math_ops.cast(grad, dtypes.float32) *
(math_ops.cast(images, dtypes.float32) -
math_ops.cast(mean, dtypes.float32)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The gradient for the contrast factor (grad_factor) is computed in dtypes.float32. If the input factor is of a different floating-point type (such as float16 or double), returning a float32 gradient will cause a dtype mismatch error during backpropagation. We should cast grad_factor back to factor.dtype before returning.

Suggested change
grad_factor = math_ops.reduce_sum(
math_ops.cast(grad, dtypes.float32) *
(math_ops.cast(images, dtypes.float32) -
math_ops.cast(mean, dtypes.float32)))
grad_factor = math_ops.cast(
math_ops.reduce_sum(
math_ops.cast(grad, dtypes.float32) *
(math_ops.cast(images, dtypes.float32) -
math_ops.cast(mean, dtypes.float32))),
factor.dtype)
References
  1. Validate tensor operations, shapes, and broadcasting logic to ensure correctness. (link)

The unknown-rank branch subtracted 3 from the Python value None
instead of the symbolic rank, which would raise TypeError whenever a
graph placeholder of unknown rank reached it. Read the rank through
array_ops.rank there, matching the intent of the branch.

Caught in code review on pull request 126086.

Test Plan:
  python -m py_compile tensorflow/python/ops/image_grad.py
  image_grad_test.py AdjustContrastOpTest against the nightly overlay:
  "Ran 4 tests in 1.393s / OK (skipped=1)".
@VaggelisGian

Copy link
Copy Markdown
Author

Handled both points in 951a567:

  • Unknown-rank branch: correct catch, thank you - the branch subtracted from the Python None instead of the symbolic rank. Now reads the rank through array_ops.rank(images) there.
  • grad_factor dtype: leaving as float32. The op def pins the factor input to contrast_factor: float (tensorflow/core/ops/image_ops.cc:582), so factor.dtype is always float32 and the extra cast could never change anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review Pull request awaiting review size:M CL Change Size: Medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tf.image.adjust_hue, adjust_saturation and adjust_contrast cannot be differentiated (no registered gradients)

2 participants