How do I use 1D gradients to compute a 2D Sobel in OpenCV with a different vector norm?

Question:

OpenCV uses an implementation of a Sobel operator defined here (details here). In this implementation, the horizontal derivative is generated, then the vertical derivative is generated, then the gradient is computed as the L2 norm of the derivatives.

Let’s say I wanted to use the L1 norm instead. In order to prove this out, I take an image and try to get the same result from OpenCV’s Sobel() that I get from manually calculating the L2 norm of the gradients:

import cv2


z_img = cv2.imread(".\some_image.tif", cv2.IMREAD_UNCHANGED)

z_px_rows = z_img.shape[0]
z_px_cols = z_img.shape[1]

print(f'Center pixel intensity (original): {z_img[z_px_rows // 2, z_px_cols // 2]}')

gx = cv2.Sobel(z_img, cv2.CV_32F, 1, 0, ksize=13)
print(f'Center pixel intensity (gx): {gx[z_px_rows // 2, z_px_cols // 2]}')

gy = cv2.Sobel(z_img, cv2.CV_32F, 0, 1, ksize=13)
print(f'Center pixel intensity (gy): {gy[z_px_rows // 2, z_px_cols // 2]}')

mag, _ = cv2.cartToPolar(gx, gy)
print(f'Center pixel intensity (homebrew sobel): {mag[z_px_rows // 2, z_px_cols // 2]}')

native_sobel = cv2.Sobel(z_img, cv2.CV_32F, 1, 1, ksize=13)
print(f'Center pixel intensity (native sobel): {native_sobel[z_px_rows // 2, z_px_cols // 2]}')

Here I’m using a 32-bit float image where the minimum is 0.0 and the maximum is around 600.0. The output of this is:

Center pixel intensity (original): 537.156982421875
Center pixel intensity (gx): -220087.90625
Center pixel intensity (gy): 350005.25
Center pixel intensity (homebrew sobel): 413451.78125
Center pixel intensity (native sobel): 16357.7548828125

Obviously, something is way off. I would expect those last two values to be the same (not exactly the same, but definitely close). I tried normalizing the pixels in the image to the range [0, 1], which didn’t help. I tried converting the images to 8-bit unsigned, which also didn’t help. What have I misunderstood about the implementation that would account for this discrepancy?

Asked By: maldata

||

Answers:

You are comparing "apples" to "oranges".

In Python/OpenCV, cv2.Sobel() computes either the X directional derivative or the Y directional derivative or a mixed derivative as follows:

enter image description here

This is not the same as the magnitude of the gradient:

enter image description here

where x(I) = X directional derivative from the Sobel,
y(I) = Y directional derivative from the Sobel with
I = src and magnitude = dst

If you want the L1 norm, then in place of the square root magnitude above, use

magnitude(I) = |x(I)| + |y(I)|

where x(I) = X directional derivative and y(I) = Y directional derivative (from Sobel) and I = src and magnitude = dot

Answered By: fmw42

I’ve accepted the answer from @fmw42 as correct, but I want to add more context.

The direct answer to my question is that I found the documentation misleading. The "Formulation" section in this link implied to me that the cv2.Sobel() method does the convolutions with the x and y kernels, and then returns the magnitude (L2 norm) of those derivatives.

The mistake that @fmw42 pointed out to me, and which I then verified, was that cv2.Sobel() does not calculate the magnitude for you. It only does the convolution using the Sobel kernels. You can verify by looking at the output of this code:

import cv2
import numpy as np


z_img = cv2.imread(".\some_image.tif", cv2.IMREAD_UNCHANGED)

z_px_rows = z_img.shape[0]
z_px_cols = z_img.shape[1]

kernel_x = np.array([[-1, 0, 1],
                     [-2, 0, 2],
                     [-1, 0, 1]])

kernel_y = np.array([[-1, -2, -1],
                     [ 0,  0,  0],
                     [ 1,  2,  1]])
  
gx = cv2.filter2D(src=z_img, ddepth=-1, kernel=kernel_x)
print(f'Center pixel intensity (gx): {gx[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_x = cv2.Sobel(z_img, cv2.CV_32F, 1, 0, ksize=3)
print(f'Center pixel intensity (native sobel x): {native_sobel_x[z_px_rows // 2, z_px_cols // 2]}')

gy = cv2.filter2D(src=z_img, ddepth=-1, kernel=kernel_y)
print(f'Center pixel intensity (gy): {gy[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_y = cv2.Sobel(z_img, cv2.CV_32F, 0, 1, ksize=3)
print(f'Center pixel intensity (native sobel y): {native_sobel_y[z_px_rows // 2, z_px_cols // 2]}')

# Now take gx and convolve using the y kernel
mixed = cv2.filter2D(src=gx, ddepth=-1, kernel=kernel_y)
print(f'Center pixel intensity (mixed): {mixed[z_px_rows // 2, z_px_cols // 2]}')

# And compare to calling Sobel(dy=1) on the output of Sobel(dx=1)
native_sobel_mixed = cv2.Sobel(native_sobel_x, cv2.CV_32F, 0, 1, ksize=3)
print(f'Center pixel intensity (native sobel mixed): {native_sobel_mixed[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_both = cv2.Sobel(z_img, cv2.CV_32F, 1, 1, ksize=3)
print(f'Center pixel intensity (native sobel): {native_sobel_both[z_px_rows // 2, z_px_cols // 2]}')

This will produce intensities of the center pixel as follows:

Description Intensity of center pixel
Manual convolution in x -0.4061279296875
cv2.Sobel(dx=1, dy=0) -0.4061279296875
Manual convolution in y 0.41986083984375
cv2.Sobel(dx=0, dy=1) 0.419921875
Manual convolution in x, then y 1.994873046875
cv2.Sobel() in x, then y 1.9949951171875
cv2.Sobel(dx=1, dy=1) 0.2659912109375

What remains a mystery to me is why calling cv2.Sobel(dx=1, dy=1) generates a different result from calling cv2.Sobel(dx=1) on the original image and then calling cv2.Sobel(dy=1) on the result of the first call (the last three rows). That seems like a different question, though.

Answered By: maldata

You question why the mixed filter2D gradient is not the same as the mixed sobel gradient with kernel dx=1 and dy=1. You have a couple of issues. 1) You did not ensure that your filter2D outputs were floats. 2) The answer is to do multiplication (element wise) between the two kernels first, then do the convolution with filter_2D (rather than successive convolutions). Note that it is element-wise multiplication rather than matrix multiplication. To show that, see the following script and results.

Input:

enter image description here

import cv2
import numpy as np


z_img = cv2.imread("lena_gray.png")

z_px_rows = z_img.shape[0]
z_px_cols = z_img.shape[1]

kernel_x = np.array([[-1, 0, 1],
                     [-2, 0, 2],
                     [-1, 0, 1]])

kernel_y = np.array([[-1, -2, -1],
                     [ 0,  0,  0],
                     [ 1,  2,  1]])
# matrix multiply            
kernel_xy1 = np.matmul(kernel_x,kernel_y)
kernel_xy2 = np.matmul(kernel_x,kernel_y)
# element-wise multiply
kernel_xy3 = cv2.multiply(kernel_x,kernel_y)
kernel_xy4 = cv2.multiply(kernel_y,kernel_x)

print('')
print("kernel_xy1:n", kernel_xy1)
print('')
print("kernel_xy2:n", kernel_xy1)
print('')
print("kernel_xy3:n", kernel_xy1)
print('')
print("kernel_xy4:n", kernel_xy1)
print('')
  
gx = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_x)
print(f'Center pixel intensity (gx): {gx[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_x = cv2.Sobel(z_img, cv2.CV_32F, 1, 0, ksize=3)
print(f'Center pixel intensity (native sobel x): {native_sobel_x[z_px_rows // 2, z_px_cols // 2]}')

gy = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_y)
print(f'Center pixel intensity (gy): {gy[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_y = cv2.Sobel(z_img, cv2.CV_32F, 0, 1, ksize=3)
print(f'Center pixel intensity (native sobel y): {native_sobel_y[z_px_rows // 2, z_px_cols // 2]}')

mixed1 = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_xy1)
print(f'Center pixel intensity (mixed1 - matrix multiply): {mixed1[z_px_rows // 2, z_px_cols // 2]}')

mixed2 = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_xy2)
print(f'Center pixel intensity (mixed2 - matrix multiply): {mixed2[z_px_rows // 2, z_px_cols // 2]}')

mixed3 = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_xy3)
print(f'Center pixel intensity (mixed3 - element-wise multiply): {mixed3[z_px_rows // 2, z_px_cols // 2]}')

mixed4 = cv2.filter2D(src=z_img, ddepth=cv2.CV_32F, kernel=kernel_xy4)
print(f'Center pixel intensity (mixed4 - element-wise multiply): {mixed4[z_px_rows // 2, z_px_cols // 2]}')

native_sobel_xy = cv2.Sobel(z_img, cv2.CV_32F, 1, 1, ksize=3)
print(f'Center pixel intensity (native sobel xy): {native_sobel_xy[z_px_rows // 2, z_px_cols // 2]}')

Results:

kernel_xy1:
 [[2 4 2]
 [4 8 4]
 [2 4 2]]

kernel_xy2:
 [[2 4 2]
 [4 8 4]
 [2 4 2]]

kernel_xy3:
 [[2 4 2]
 [4 8 4]
 [2 4 2]]

kernel_xy4:
 [[2 4 2]
 [4 8 4]
 [2 4 2]]

Center pixel intensity (gx): [6. 6. 6.]
Center pixel intensity (native sobel x): [6. 6. 6.]
Center pixel intensity (gy): [-184. -184. -184.]
Center pixel intensity (native sobel y): [-184. -184. -184.]
Center pixel intensity (mixed1 - matrix multiply): [2580. 2580. 2580.]
Center pixel intensity (mixed2 - matrix multiply): [2580. 2580. 2580.]
Center pixel intensity (mixed3 - element-wise multiply): [-28. -28. -28.]
Center pixel intensity (mixed4 - element-wise multiply): [-28. -28. -28.]
Center pixel intensity (native sobel xy): [-28. -28. -28.]
Answered By: fmw42
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.