Skip to content

[Keras 3 OpenVINO backend] Support gaussian_blur operation#22668

Closed
daehyun99 wants to merge 6 commits intokeras-team:masterfrom
daehyun99:gaussian_blur-1
Closed

[Keras 3 OpenVINO backend] Support gaussian_blur operation#22668
daehyun99 wants to merge 6 commits intokeras-team:masterfrom
daehyun99:gaussian_blur-1

Conversation

@daehyun99
Copy link
Copy Markdown
Contributor

Description

Contributor Agreement

Please check all boxes below before submitting your PR for review:

  • I am a human, and not a bot.
  • I will be responsible for responding to review comments in a timely manner.
  • I will work with the maintainers to push this PR forward until submission.

Note: Failing to adhere to this agreement may result in your future PRs no longer being reviewed.

Copy link
Copy Markdown
Contributor

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

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 removes several gaussian_blur tests from the OpenVINO backend's exclusion list. However, the gaussian_blur operation has not yet been implemented in the backend, which will cause the tests to fail with a NotImplementedError. You should include the implementation of the operation in this PR to avoid breaking the CI.

Comment thread keras/src/backend/openvino/excluded_concrete_tests.txt
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 12, 2026

Codecov Report

❌ Patch coverage is 0% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.81%. Comparing base (75d7bea) to head (8d79c8d).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
keras/src/backend/openvino/image.py 0.00% 41 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (75d7bea) and HEAD (8d79c8d). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (75d7bea) HEAD (8d79c8d)
keras 5 3
keras-openvino 1 0
keras-tensorflow 1 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #22668       +/-   ##
===========================================
- Coverage   83.15%   68.81%   -14.35%     
===========================================
  Files         596      596               
  Lines       68858    69030      +172     
  Branches    10774    10795       +21     
===========================================
- Hits        57259    47502     -9757     
- Misses       8783    18973    +10190     
+ Partials     2816     2555      -261     
Flag Coverage Δ
keras 68.68% <0.00%> (-14.30%) ⬇️
keras-jax 58.90% <0.00%> (-0.15%) ⬇️
keras-numpy 54.74% <0.00%> (-0.14%) ⬇️
keras-openvino ?
keras-tensorflow ?
keras-torch 59.24% <0.00%> (-0.15%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread keras/src/backend/openvino/excluded_concrete_tests.txt Outdated
@daehyun99
Copy link
Copy Markdown
Contributor Author

/gemini review

Copy link
Copy Markdown
Contributor

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

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 implements the gaussian_blur operation for the OpenVINO backend and enables its corresponding tests. Feedback indicates that the global aliasing of bfloat16 to float32 in the common utilities is a breaking change and should be moved to backend-specific code. The gaussian_blur implementation requires several fixes: ensuring floating-point precision during kernel generation, correcting the 2D kernel construction for non-square shapes, and resolving type mismatches where tensors are passed to OpenVINO operations expecting Python scalars or lists. Additionally, the data_format should be standardized using existing backend utilities.

Comment thread keras/src/backend/common/dtypes.py Outdated
str: "string",
# special case for string value
"int": "int64" if config.backend() == "tensorflow" else "int32",
"bfloat16": "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.

critical

This change globally aliases the "bfloat16" dtype string to "float32" in the common Keras utilities. This is a breaking change that affects all backends, preventing users from actually using bfloat16 even when supported (e.g., in JAX or TensorFlow). If the OpenVINO backend has specific limitations with bfloat16, those should be handled within the OpenVINO-specific implementation (e.g., in cast or convert_to_tensor), not in the shared common/dtypes.py file.

def _create_gaussian_kernel(kernel_size, sigma, ov_type):
def _get_gaussian_kernel1d(size, sigma):
x = (
ov_opset.range(0, size, 1, output_type=ov_type).output(0)
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 Gaussian kernel calculation should be performed in floating-point precision (e.g., f32) to avoid significant precision loss during the exp and normalization steps. If ov_type is an integral type (like u8), the range and subsequent arithmetic will produce incorrect results. The kernel should only be converted to the target ov_type after the full 2D kernel is computed and normalized.

Suggested change
ov_opset.range(0, size, 1, output_type=ov_type).output(0)
ov_opset.range(0, size, 1, output_type=Type.f32).output(0)

Comment on lines +748 to +763
def _get_gaussian_kernel2d(size, sigma):
kernel1d_x = _get_gaussian_kernel1d(size[0], sigma[0])
kernel1d_y = _get_gaussian_kernel1d(size[1], sigma[1])

kernel1d_x = ov_opset.reshape(
kernel1d_x,
ov_opset.constant([1, int(size[1])], Type.i32).output(0),
False,
).output(0)

kernel1d_y = ov_opset.reshape(
kernel1d_y,
ov_opset.constant([int(size[0]), 1], Type.i32).output(0),
False,
).output(0)
return ov_opset.multiply(kernel1d_y, kernel1d_x).output(0)
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 2D kernel construction logic is incorrect for non-square kernels and contains problematic int() calls on tensors.

  1. kernel1d_x is generated using size[0] (Height) but then reshaped using size[1] (Width) at line 754. This will fail if Height != Width.
  2. Calling int() on an OpenVINOKerasTensor (like size[0]) triggers convert_to_numpy(), which compiles and executes a sub-graph during model construction. This is extremely inefficient and will fail for dynamic shapes.

You should use the original kernel_size tuple for static dimensions or use -1 in reshape to avoid Python-side evaluation.

Suggested change
def _get_gaussian_kernel2d(size, sigma):
kernel1d_x = _get_gaussian_kernel1d(size[0], sigma[0])
kernel1d_y = _get_gaussian_kernel1d(size[1], sigma[1])
kernel1d_x = ov_opset.reshape(
kernel1d_x,
ov_opset.constant([1, int(size[1])], Type.i32).output(0),
False,
).output(0)
kernel1d_y = ov_opset.reshape(
kernel1d_y,
ov_opset.constant([int(size[0]), 1], Type.i32).output(0),
False,
).output(0)
return ov_opset.multiply(kernel1d_y, kernel1d_x).output(0)
def _get_gaussian_kernel2d(size, sigma):
kernel1d_h = _get_gaussian_kernel1d(size[0], sigma[0])
kernel1d_w = _get_gaussian_kernel1d(size[1], sigma[1])
kernel1d_h = ov_opset.reshape(
kernel1d_h,
ov_opset.constant([-1, 1], Type.i32).output(0),
False,
).output(0)
kernel1d_w = ov_opset.reshape(
kernel1d_w,
ov_opset.constant([1, -1], Type.i32).output(0),
False,
).output(0)
return ov_opset.multiply(kernel1d_h, kernel1d_w).output(0)

Comment on lines +802 to +806
ov_opset.constant(
[1, 1, kernel_size[0], kernel_size[1]], Type.i32
).output(0),
False,
).output(0)
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

ov_opset.constant expects a numpy array or a list of scalars; it does not accept OpenVINOKerasTensor objects. Since kernel_size was converted to a tensor at line 772, kernel_size[0] is a tensor, and this call will fail. You should use the original kernel_size tuple values here.

Suggested change
ov_opset.constant(
[1, 1, kernel_size[0], kernel_size[1]], Type.i32
).output(0),
False,
).output(0)
ov_opset.constant(
[1, 1, int(kernel_size_tuple[0]), int(kernel_size_tuple[1])], Type.i32
).output(0),

Comment on lines +824 to +828
[(kernel_size[0] - 1) // 2, (kernel_size[1] - 1) // 2],
[
kernel_size[0] - 1 - (kernel_size[0] - 1) // 2,
kernel_size[1] - 1 - (kernel_size[1] - 1) // 2,
],
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 pads_begin and pads_end arguments for ov_opset.group_convolution must be a List[int]. Because kernel_size is a tensor, the expression (kernel_size[0] - 1) // 2 results in an OpenVINOKerasTensor, which will cause a TypeError. Use the original Python integers from the kernel_size input for these calculations.

Suggested change
[(kernel_size[0] - 1) // 2, (kernel_size[1] - 1) // 2],
[
kernel_size[0] - 1 - (kernel_size[0] - 1) // 2,
kernel_size[1] - 1 - (kernel_size[1] - 1) // 2,
],
[(int(kernel_size_tuple[0]) - 1) // 2, (int(kernel_size_tuple[1]) - 1) // 2],
[
int(kernel_size_tuple[0]) - 1 - (int(kernel_size_tuple[0]) - 1) // 2,
int(kernel_size_tuple[1]) - 1 - (int(kernel_size_tuple[1]) - 1) // 2,
],

@@ -726,9 +726,119 @@ def is_valid(index, size_node):
def gaussian_blur(
images, kernel_size=(3, 3), sigma=(1.0, 1.0), data_format=None
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.

medium

The data_format argument should be standardized to ensure consistency and handle the None case (defaulting to the global Keras image data format).

    images, kernel_size=(3, 3), sigma=(1.0, 1.0), data_format=None
):
    data_format = backend.standardize_data_format(data_format)

Comment thread keras/src/backend/common/dtypes.py Outdated
@daehyun99
Copy link
Copy Markdown
Contributor Author

Close this PR

@daehyun99 daehyun99 closed this Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Keras 3 OpenVINO backend] Support gaussian_blur operation

5 participants