Repository: scipy/scipy

Issue:
DOC: clarify that scipy.ndimage.sobel does not compute the 2D magnitude
### Describe your issue.

scipy.ndimage.sobel is produces very wrong result.
The example shown in https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.sobel.html is NOT what a Sobel filtered image looks like.
The current code reproduces the image in the example exactly (again, this is not what you want).

See https://scikit-image.org/docs/stable/api/skimage.filters.html#skimage.filters.sobel for correct implementation (though a little funny with the scaling)
Or https://en.wikipedia.org/wiki/Sobel_operator for basic implementation.

Or, use (not optimised):
    from scipy import signal
    import numpy as np
    Gx = np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]])
    Gy = Gx.T
    gradx = signal.convolve2d(img, Gx, boundary='symm', mode='same')
    grady = signal.convolve2d(img, Gy, boundary='symm', mode='same')
    G = np.sqrt(gradx**2 + grady**2)


### Reproducing Code Example

```python
from scipy import ndimage, datasets
import matplotlib.pyplot as plt
fig = plt.figure()
plt.gray()  # show the filtered result in grayscale
ax1 = fig.add_subplot(121)  # left side
ax2 = fig.add_subplot(122)  # right side
ascent = datasets.ascent()
result = ndimage.sobel(ascent)
ax1.imshow(ascent)
ax2.imshow(result)
plt.show()
```


### Error message

```shell
Runs, but produces wrong result.
```


### SciPy/NumPy/Python version and system information

```shell
1.10.0 1.23.5 sys.version_info(major=3, minor=10, micro=9, releaselevel='final', serial=0)
lapack_mkl_info:
  NOT AVAILABLE
openblas_lapack_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/rorys/anaconda3/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/rorys/anaconda3/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
blas_mkl_info:
  NOT AVAILABLE
blis_info:
  NOT AVAILABLE
openblas_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/rorys/anaconda3/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
blas_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/rorys/anaconda3/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
Supported SIMD extensions in this NumPy install:
    baseline = SSE,SSE2,SSE3
    found = SSSE3,SSE41,POPCNT,SSE42
    not found = AVX,F16C,FMA3,AVX2,AVX512F,AVX512CD,AVX512_KNL,AVX512_SKX,AVX512_CLX,AVX512_CNL,AVX512_ICL
```



Discussion:
Comparison of scipy and skimage's default sobel filter:

![image](https://user-images.githubusercontent.com/40656107/236572639-7e46487d-8a03-45de-bad5-84438e43252f.png)

```python
from scipy import ndimage, datasets
import matplotlib.pyplot as plt
from skimage import filters
fig = plt.figure()
plt.gray()  # show the filtered result in grayscale
ax1 = fig.add_subplot(131)  # left side
ax2 = fig.add_subplot(132)  # right side
ax3 = fig.add_subplot(133)  # right side
ascent = datasets.ascent()
ax1.imshow(ascent)
ax2.imshow(ndimage.sobel(ascent))
ax2.set_title("scipy sobel")
ax3.imshow(filters.sobel(ascent))
ax3.set_title("skimage sobel")
for ax in [ax1, ax2, ax3]:
    ax.set_axis_off()
plt.show()
```

The skimage result looks definitely much better. I am not exactly an expert on `ndimage`, is this a parameter issue?
> Comparison of scipy and skimage's default sobel filter:
> 
> ![image](https://user-images.githubusercontent.com/40656107/236572639-7e46487d-8a03-45de-bad5-84438e43252f.png)
> 
> ```python
> from scipy import ndimage, datasets
> import matplotlib.pyplot as plt
> from skimage import filters
> fig = plt.figure()
> plt.gray()  # show the filtered result in grayscale
> ax1 = fig.add_subplot(131)  # left side
> ax2 = fig.add_subplot(132)  # right side
> ax3 = fig.add_subplot(133)  # right side
> ascent = datasets.ascent()
> ax1.imshow(ascent)
> ax2.imshow(ndimage.sobel(ascent))
> ax2.set_title("scipy sobel")
> ax3.imshow(filters.sobel(ascent))
> ax3.set_title("skimage sobel")
> for ax in [ax1, ax2, ax3]:
>     ax.set_axis_off()
> plt.show()
> ```
> 
> The skimage result looks definitely much better. I am not exactly an expert on `ndimage`, is this a parameter issue?

The source code uses the line

```
    correlate1d(input, [-1, 0, 1], axis, output, modes[axis], cval, 0)
```

but based on the wikipedia article for the Sobel operator, I think it's actually supposed to be `[1, 0, -1]`. 
Trying out the code, it doesn't seem like changing the weight helped much, actually.

```
import numpy as np
from numpy.core.multiarray import normalize_axis_index
from scipy import ndimage, datasets
from scipy.ndimage import _ni_support, _nd_image, _ni_docstrings, correlate1d
import matplotlib.pyplot as plt
fig = plt.figure()
plt.gray()
ax1 = fig.add_subplot(131)
ax2 = fig.add_subplot(132)
ax3 = fig.add_subplot(133)
ascent = datasets.ascent()

result = np.asarray(ascent)
#Hardcoded the default variables here
axis = normalize_axis_index(-1, result.ndim)
output = _ni_support._get_output(None, result)
modes = _ni_support._normalize_sequence("reflect", result.ndim)
correlate1d(result, [1, 0, -1], axis, output, modes[axis], 0.0, 0)

axes = [ii for ii in range(result.ndim) if ii != axis]
for ii in axes:
    correlate1d(output, [1, 2, 1], ii, output, modes[ii], 0.0, 0)

ax1.imshow(ascent)
ax2.imshow(ndimage.sobel(ascent))
ax3.imshow(output)

ax2.set_title("Before Change")
ax3.set_title("After Change")
plt.show()
```
![change](https://user-images.githubusercontent.com/33975162/236577887-11e9a607-fe87-467c-8ac1-68d7ebedc0f9.png)

This post helped: https://stackoverflow.com/questions/7185655/applying-the-sobel-filter-using-scipy

The scipy function only applies a 1D Sobel filter. To get what is usually meant by a Sobel filter in image processing, one needs a few more lines:

```python
from scipy import ndimage, datasets
import matplotlib.pyplot as plt
from skimage import filters

def sobel_2d(image):
    image = image.astype('int32')
    dx = ndimage.sobel(image, 0)  # horizontal derivative
    dy = ndimage.sobel(image, 1)  # vertical derivative
    mag = np.hypot(dx, dy)  # magnitude
    mag *= 255.0 / np.max(mag)
    return mag

fig = plt.figure()
plt.gray()  # show the filtered result in grayscale
ax1 = fig.add_subplot(141)
ax2 = fig.add_subplot(142)
ax3 = fig.add_subplot(143)
ax4 = fig.add_subplot(144)


ascent = datasets.ascent()
ax1.imshow(ascent)
ax2.imshow(ndimage.sobel(ascent))
ax2.set_title("scipy sobel")
ax3.imshow(filters.sobel(ascent))
ax3.set_title("skimage sobel")
ax4.imshow(sobel_2d(ascent))
ax4.set_title("scipy sobel 2D")
for ax in [ax1, ax2, ax3, ax4]:
    ax.set_axis_off()
plt.show()
``` 

![image](https://user-images.githubusercontent.com/40656107/236578235-abb5c074-03be-4ec5-a0b7-9984f6022564.png)

The way the example is written, a user will assume though that `ndimage.sobel` directly gives the regular sobel filter output. This should be improved.

FWIW, the magnitude of the two is not "the regular Sobel filter". Personally, I consider the individual axis-specific gradients to be "the Sobel filters". `skimage` happened to give the unadorned name `sobel` to the magnitude version while relegating its axis-specific ones to `sobel_h` and `sobel_v`. `ndimage` doesn't do the same because we deal with N dimensions, not two, so `sobel` happens to do the axis-specific ones. [OpenCV also uses the unadorned `Sobel` for the axis-specific directional gradients](https://docs.opencv.org/3.4/d2/d2c/tutorial_sobel_derivatives.html) and makes you combine them to get the magnitude. Both are reasonable choices for naming, but we could be clearer in our docstring.

Resolving patch:
diff --git a/scipy/ndimage/_filters.py b/scipy/ndimage/_filters.py
--- a/scipy/ndimage/_filters.py
+++ b/scipy/ndimage/_filters.py
@@ -433,19 +433,35 @@ def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0):
     %(mode_multiple)s
     %(cval)s
 
+    Notes
+    -----
+    This function computes the axis-specific Sobel gradient.
+    The horizontal edges can emphasised with the horizontal trasform (axis=0),
+    the vertical edges with the vertical transform (axis=1) and so on for higher
+    dimensions. These can be combined to give the magnitude.
+
     Examples
     --------
     >>> from scipy import ndimage, datasets
     >>> import matplotlib.pyplot as plt
-    >>> fig = plt.figure()
+    >>> import numpy as np
+    >>> ascent = datasets.ascent().astype('int32')
+    >>> sobel_h = ndimage.sobel(ascent, 0)  # horizontal gradient
+    >>> sobel_v = ndimage.sobel(ascent, 1)  # vertical gradient
+    >>> magnitude = np.sqrt(sobel_h**2 + sobel_v**2)
+    >>> magnitude *= 255.0 / np.max(magnitude)  # normalization
+    >>> fig, axs = plt.subplots(2, 2, figsize=(8, 8))
     >>> plt.gray()  # show the filtered result in grayscale
-    >>> ax1 = fig.add_subplot(121)  # left side
-    >>> ax2 = fig.add_subplot(122)  # right side
-    >>> ascent = datasets.ascent()
-    >>> result = ndimage.sobel(ascent)
-    >>> ax1.imshow(ascent)
-    >>> ax2.imshow(result)
+    >>> axs[0, 0].imshow(ascent)
+    >>> axs[0, 1].imshow(sobel_h)
+    >>> axs[1, 0].imshow(sobel_v)
+    >>> axs[1, 1].imshow(magnitude)
+    >>> titles = ["original", "horizontal", "vertical", "magnitude"]
+    >>> for i, ax in enumerate(axs.ravel()):
+    ...     ax.set_title(titles[i])
+    ...     ax.axis("off")
     >>> plt.show()
+
     """
     input = numpy.asarray(input)
     axis = normalize_axis_index(axis, input.ndim)


Test patch: