Repository: scikit-image/scikit-image

Issue:
skeletonize_3d output pixel values are doubled from input pixel values
### Description:

`skeletonize` seems to preserve the pixel value of the input image. This is fine.

`skeletonize_3d` seems to *double* the pixel value of the input image. Not sure if this is considered a bug, but depending on the pixel value and pixel data type it can overflow and be confusing.


### Way to reproduce:

Slightly modified from [example](https://scikit-image.org/docs/stable/auto_examples/edges/plot_skeleton.html):
```python
from skimage.morphology import skeletonize, skeletonize_3d
from skimage import data
import matplotlib.pyplot as plt
from skimage.util import invert

image = invert(data.horse()) * 55

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
print(skeleton_3d.max())
```

Output:
```
55
110
```

### Version information:

```Shell
3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]
Windows-10-10.0.19045-SP0
scikit-image version: 0.21.0
numpy version: 1.24.3
```
```



Discussion:
Thanks @petsuter for reporting this. I can not reproduce this. 
```
from skimage.morphology import skeletonize, skeletonize_3d
from skimage import data
import matplotlib.pyplot as plt
from skimage.util import invert

image = invert(data.horse()) * 55

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
True
print(skeleton_3d.max())
55
```

`skeletonize` returns a boolean which is the expected [behavior](https://github.com/scikit-image/scikit-image/blob/49a98547e909697f6ee89ec70691c7cad7eeb823/skimage/morphology/_skeletonize.py#L79C1-L80C66) given `image.ndim == 2` and `method` is None.

Did I miss something?


Hi @decorouz
Thanks for trying to reproduce. Indeed, today I get the same results as you, which seem fine. I don't know why I got different results before. I will investigate what happened when I'm back on the other computer.
Ah, sorry for the confusion. The problem occurs with `image = image.astype('int8')`, but for some reason I messed up copying the example above.

```python
from skimage.morphology import skeletonize, skeletonize_3d
from skimage import data
import matplotlib.pyplot as plt
from skimage.util import invert

image = (invert(data.horse()) * 55)

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
print(skeleton_3d.max())

image = image.astype('int8')

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
print(skeleton_3d.max())
```

Output:
```
Warning (from warnings module):
  File "C:\Python311\Lib\site-packages\skimage\util\dtype.py", line 550
    return _convert(image, np.uint8, force_copy)
UserWarning: Downcasting int32 to uint8 without scaling because max value 55 fits in uint8
True
55
True
110
```
Not sure if the warning is related.

----
(55 is just a random value that would probably not occur in practice.) But the value 255 is quite commonly used, and with that value it's even worse:
```
from skimage.morphology import skeletonize, skeletonize_3d
from skimage import data
import matplotlib.pyplot as plt
from skimage.util import invert

image = (invert(data.horse()) * 255)

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
print(skeleton_3d.max())

image = image.astype('int8')

skeleton = skeletonize(image)
skeleton_3d = skeletonize_3d(image)

print(skeleton.max())
print(skeleton_3d.max(), skeleton_3d.min())
```

```
True
255
True
0 0
```
Presumably due to overflow there are no results at all.
Oof, that's nasty. The culprit here is our [data type range conversion](https://scikit-image.org/docs/dev/user_guide/data_types.html), as per usual, which is why we will no longer do them in skimage2. The line that does the conversion is here:

https://github.com/scikit-image/scikit-image/blob/8c3c324be43a057643fee8d0c92253e82a837223/skimage/morphology/_skeletonize.py#L621

You can verify the problem easily:

```python
In[1]: import numpy as np
In [2]: np.uint8(255).astype(np.int8)  # overflow happens here!
Out[2]: -1

In [3]: import skimage as ski
In [4]: ski.util.img_as_ubyte(np.uint8(255).astype(np.int8))  # clips negative values!
Out[4]: array(0, dtype=uint8)

In [5]: ski.util.img_as_ubyte(np.uint8(55).astype(np.int8))  # positive values are doubled according to dtype range
Out[5]: array(110, dtype=uint8)
```

Given the docstring of skeletonize3d for the input image:

> A binary image containing the objects to be skeletonized. Zeros represent background, nonzero values are foreground.

My suggestion is that we modify the preprocessing code to do as described:

```
if image.dtype is not np.dtype('bool'):
    image = (image != 0)
```

(Removing also the now-redundant `np.ascontiguousarray` — this is not needed because `np.pad` will make a new array anyway.)
Hmm, the doubling originates from https://github.com/scikit-image/scikit-image/blob/b6be0cec15a2f3ecbfc58eee9960077a5e91efb6/skimage/util/dtype.py#L361

From [`skimage.util.img_as_ubyte`'s docstring](https://scikit-image.org/docs/dev/api/skimage.util.html#skimage.util.img_as_ubyte)

> Positive values are scaled between 0 and 255.

I find the docstring and the implementation of `skeletonize_3d` conflicting. The docstring expects a binary image, but the implementation silently accepts other types and tries to return the output range. The function has only a single doctest so there is not much that indicates the intended API.

I am not sure if that is the best option but what do you think about deprecating in- and output dtypes other than bool? I find the current behavior unexpected and unnecessarily complicated. 
Haha, just saw your comment now @jni. :smile: 

Resolving patch:
diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py
--- a/skimage/morphology/_skeletonize.py
+++ b/skimage/morphology/_skeletonize.py
@@ -5,7 +5,7 @@
 from scipy import ndimage as ndi
 
 from .._shared.utils import check_nD, deprecate_kwarg
-from ..util import crop, img_as_ubyte
+from ..util import crop
 from ._skeletonize_3d_cy import _compute_thin_image
 from ._skeletonize_cy import _fast_skeletonize, _skeletonize_loop, _table_lookup_index
 
@@ -28,7 +28,7 @@ def skeletonize(image, *, method=None):
 
     Returns
     -------
-    skeleton : ndarray
+    skeleton : ndarray of bool
         The thinned image.
 
     See Also
@@ -48,8 +48,8 @@ def skeletonize(image, *, method=None):
     Examples
     --------
     >>> X, Y = np.ogrid[0:9, 0:9]
-    >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(np.uint8)
-    >>> ellipse
+    >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool)
+    >>> ellipse.view(np.uint8)
     array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
@@ -60,7 +60,7 @@ def skeletonize(image, *, method=None):
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
            [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
     >>> skel = skeletonize(ellipse)
-    >>> skel.astype(np.uint8)
+    >>> skel.view(np.uint8)
     array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
@@ -72,12 +72,14 @@ def skeletonize(image, *, method=None):
            [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
 
     """
+    image = image.astype(bool, copy=False)
+
     if method not in {'zhang', 'lee', None}:
         raise ValueError(
             f'skeletonize method should be either "lee" or "zhang", ' f'got {method}.'
         )
     if image.ndim == 2 and (method is None or method == 'zhang'):
-        skeleton = skeletonize_2d(image.astype(bool, copy=False))
+        skeleton = skeletonize_2d(image)
     elif image.ndim == 3 and method == 'zhang':
         raise ValueError('skeletonize method "zhang" only works for 2D ' 'images.')
     elif image.ndim == 3 or (image.ndim == 2 and method == 'lee'):
@@ -135,8 +137,8 @@ def skeletonize_2d(image):
     Examples
     --------
     >>> X, Y = np.ogrid[0:9, 0:9]
-    >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(np.uint8)
-    >>> ellipse
+    >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool)
+    >>> ellipse.view(np.uint8)
     array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
@@ -147,7 +149,7 @@ def skeletonize_2d(image):
            [0, 0, 1, 1, 1, 1, 1, 0, 0],
            [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
     >>> skel = skeletonize(ellipse)
-    >>> skel.astype(np.uint8)
+    >>> skel.view(np.uint8)
     array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
@@ -216,529 +218,37 @@ def G3p(n):
     return g123_lut, g123p_lut
 
 
-G123_LUT = np.array(
-    [
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        0,
-        1,
-        0,
-        0,
-        0,
-    ],
-    dtype=bool,
-)
-
-G123P_LUT = np.array(
-    [
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        1,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        1,
-        1,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        1,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        1,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-        0,
-    ],
-    dtype=bool,
-)
+# fmt: off
+G123_LUT = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+                     0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
+                     0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
+                     0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                     0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+                     1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0,
+                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0,
+                     0, 1, 1, 0, 0, 1, 0, 0, 0], dtype=bool)
+
+G123P_LUT = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0,
+                      0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
+                      1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1,
+                      0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                      0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=bool)
+# fmt: on
 
 
 def thin(image, max_num_iter=None):
@@ -748,7 +258,9 @@ def thin(image, max_num_iter=None):
     Parameters
     ----------
     image : binary (M, N) ndarray
-        The image to be thinned.
+        The image to thin. If this input isn't already a binary image,
+        it gets converted into one: In this case, zero values are considered
+        background (False), nonzero values are considered foreground (True).
     max_num_iter : int, number of iterations, optional
         Regardless of the value of this parameter, the thinned image
         is returned immediately if an iteration produces no change.
@@ -786,10 +298,10 @@ def thin(image, max_num_iter=None):
 
     Examples
     --------
-    >>> square = np.zeros((7, 7), dtype=np.uint8)
+    >>> square = np.zeros((7, 7), dtype=bool)
     >>> square[1:-1, 2:-2] = 1
     >>> square[0, 1] =  1
-    >>> square
+    >>> square.view(np.uint8)
     array([[0, 1, 0, 0, 0, 0, 0],
            [0, 0, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 0, 0],
@@ -798,7 +310,7 @@ def thin(image, max_num_iter=None):
            [0, 0, 1, 1, 1, 0, 0],
            [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
     >>> skel = thin(square)
-    >>> skel.astype(np.uint8)
+    >>> skel.view(np.uint8)
     array([[0, 1, 0, 0, 0, 0, 0],
            [0, 0, 1, 0, 0, 0, 0],
            [0, 0, 0, 1, 0, 0, 0],
@@ -811,7 +323,7 @@ def thin(image, max_num_iter=None):
     check_nD(image, 2)
 
     # convert image to uint8 with values in {0, 1}
-    skel = np.asanyarray(image, dtype=bool).astype(np.uint8)
+    skel = np.asanyarray(image, dtype=bool).view(np.uint8)
 
     # neighborhood mask
     mask = np.array([[8, 4, 2], [16, 0, 1], [32, 64, 128]], dtype=np.uint8)
@@ -852,7 +364,10 @@ def medial_axis(image, mask=None, return_distance=False, *, rng=None):
     Parameters
     ----------
     image : binary ndarray, shape (M, N)
-        The image of the shape to be skeletonized.
+        The image of the shape to skeletonize. If this input isn't already a
+        binary image, it gets converted into one: In this case, zero values are
+        considered background (False), nonzero values are considered
+        foreground (True).
     mask : binary ndarray, shape (M, N), optional
         If a mask is given, only those elements in `image` with a true
         value in `mask` are used for computing the medial axis.
@@ -905,9 +420,9 @@ def medial_axis(image, mask=None, return_distance=False, *, rng=None):
 
     Examples
     --------
-    >>> square = np.zeros((7, 7), dtype=np.uint8)
+    >>> square = np.zeros((7, 7), dtype=bool)
     >>> square[1:-1, 2:-2] = 1
-    >>> square
+    >>> square.view(np.uint8)
     array([[0, 0, 0, 0, 0, 0, 0],
            [0, 0, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 0, 0],
@@ -915,7 +430,7 @@ def medial_axis(image, mask=None, return_distance=False, *, rng=None):
            [0, 0, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 0, 0],
            [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
-    >>> medial_axis(square).astype(np.uint8)
+    >>> medial_axis(square).view(np.uint8)
     array([[0, 0, 0, 0, 0, 0, 0],
            [0, 0, 1, 0, 1, 0, 0],
            [0, 0, 0, 1, 0, 0, 0],
@@ -1088,7 +603,7 @@ def skeletonize_3d(image):
 
     Returns
     -------
-    skeleton : ndarray
+    skeleton : ndarray of bool
         The thinned image.
 
     See Also
@@ -1121,27 +636,21 @@ def skeletonize_3d(image):
             "skeletonize_3d can only handle 2D or 3D images; "
             f"got image.ndim = {image.ndim} instead."
         )
-    image = np.ascontiguousarray(image)
-    image = img_as_ubyte(image, force_copy=False)
 
-    # make an in image 3D and pad it w/ zeros to simplify dealing w/ boundaries
+    image_io = image.astype(bool, copy=False)
+
+    # make a 2D input image 3D and pad it w/ zeros to simplify dealing w/ boundaries
     # NB: careful here to not clobber the original *and* minimize copying
-    image_o = image
     if image.ndim == 2:
-        image_o = image[np.newaxis, ...]
-    image_o = np.pad(image_o, pad_width=1, mode='constant')
-
-    # normalize to binary
-    maxval = image_o.max()
-    image_o[image_o != 0] = 1
+        image_io = image_io[np.newaxis, ...]
+    image_o = np.pad(image_io, pad_width=1, mode='constant')
 
     # do the computation
-    image_o = np.asarray(_compute_thin_image(image_o))
+    image_o = _compute_thin_image(image_o)
 
     # crop it back and restore the original intensity range
     image_o = crop(image_o, crop_width=1)
     if image.ndim == 2:
         image_o = image_o[0]
-    image_o *= maxval
 
     return image_o


Test patch:
diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py
--- a/skimage/morphology/tests/test_skeletonize.py
+++ b/skimage/morphology/tests/test_skeletonize.py
@@ -17,32 +17,32 @@ def test_skeletonize_no_foreground(self):
         assert_array_equal(result, np.zeros((5, 5)))
 
     def test_skeletonize_wrong_dim1(self):
-        im = np.zeros(5)
+        im = np.zeros(5, dtype=bool)
         with pytest.raises(ValueError):
             skeletonize(im)
 
     def test_skeletonize_wrong_dim2(self):
-        im = np.zeros((5, 5, 5))
+        im = np.zeros((5, 5, 5), dtype=bool)
         with pytest.raises(ValueError):
             skeletonize(im, method='zhang')
 
     def test_skeletonize_wrong_method(self):
-        im = np.ones((5, 5))
+        im = np.ones((5, 5), dtype=bool)
         with pytest.raises(ValueError):
             skeletonize(im, method='foo')
 
     def test_skeletonize_all_foreground(self):
-        im = np.ones((3, 4))
+        im = np.ones((3, 4), dtype=bool)
         skeletonize(im)
 
     def test_skeletonize_single_point(self):
-        im = np.zeros((5, 5), np.uint8)
+        im = np.zeros((5, 5), dtype=bool)
         im[3, 3] = 1
         result = skeletonize(im)
         assert_array_equal(result, im)
 
     def test_skeletonize_already_thinned(self):
-        im = np.zeros((5, 5), np.uint8)
+        im = np.zeros((5, 5), dtype=bool)
         im[3, 1:-1] = 1
         im[2, -1] = 1
         im[4, 0] = 1
@@ -59,14 +59,15 @@ def test_skeletonize_output(self):
         expected = np.load(fetch("data/bw_text_skeleton.npy"))
         assert_array_equal(result, expected)
 
-    def test_skeletonize_num_neighbors(self):
+    @pytest.mark.parametrize("dtype", [bool, float, int])
+    def test_skeletonize_num_neighbors(self, dtype):
         # an empty image
-        image = np.zeros((300, 300))
+        image = np.zeros((300, 300), dtype=dtype)
 
         # foreground object 1
-        image[10:-10, 10:100] = 1
-        image[-100:-10, 10:-10] = 1
-        image[10:-10, -100:-10] = 1
+        image[10:-10, 10:100] = 2
+        image[-100:-10, 10:-10] = 2
+        image[10:-10, -100:-10] = 2
 
         # foreground object 2
         rs, cs = draw.line(250, 150, 10, 280)
@@ -74,7 +75,7 @@ def test_skeletonize_num_neighbors(self):
             image[rs + i, cs] = 1
         rs, cs = draw.line(10, 150, 250, 280)
         for i in range(20):
-            image[rs + i, cs] = 1
+            image[rs + i, cs] = 3
 
         # foreground object 3
         ir, ic = np.indices(image.shape)
@@ -90,7 +91,7 @@ def test_skeletonize_num_neighbors(self):
         assert not np.any(blocks == 4)
 
     def test_lut_fix(self):
-        im = np.zeros((6, 6), np.uint8)
+        im = np.zeros((6, 6), dtype=bool)
         im[1, 2] = 1
         im[2, 2] = 1
         im[2, 3] = 1
@@ -108,7 +109,7 @@ def test_lut_fix(self):
                 [0, 0, 0, 0, 0, 1],
                 [0, 0, 0, 0, 0, 0],
             ],
-            dtype=np.uint8,
+            dtype=bool,
         )
         assert np.all(result == expected)
 
@@ -120,22 +121,25 @@ def input_image(self):
         ii = np.array(
             [
                 [0, 0, 0, 0, 0, 0, 0],
-                [0, 1, 1, 1, 1, 1, 0],
+                [0, 1, 2, 3, 4, 5, 0],
                 [0, 1, 0, 1, 1, 1, 0],
                 [0, 1, 1, 1, 1, 1, 0],
-                [0, 1, 1, 1, 1, 1, 0],
+                [0, 6, 1, 1, 1, 1, 0],
                 [0, 1, 1, 1, 1, 1, 0],
                 [0, 0, 0, 0, 0, 0, 0],
             ],
-            dtype=np.uint8,
+            dtype=float,
         )
         return ii
 
     def test_zeros(self):
-        assert np.all(thin(np.zeros((10, 10))) == False)
+        image = np.zeros((10, 10), dtype=bool)
+        assert np.all(thin(image) == False)
 
-    def test_iter_1(self):
-        result = thin(self.input_image, 1).astype(np.uint8)
+    @pytest.mark.parametrize("dtype", [bool, float, int])
+    def test_iter_1(self, dtype):
+        image = self.input_image.astype(dtype)
+        result = thin(image, 1).astype(bool)
         expected = np.array(
             [
                 [0, 0, 0, 0, 0, 0, 0],
@@ -146,12 +150,14 @@ def test_iter_1(self):
                 [0, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 0, 0, 0, 0],
             ],
-            dtype=np.uint8,
+            dtype=bool,
         )
         assert_array_equal(result, expected)
 
-    def test_noiter(self):
-        result = thin(self.input_image).astype(np.uint8)
+    @pytest.mark.parametrize("dtype", [bool, float, int])
+    def test_noiter(self, dtype):
+        image = self.input_image.astype(dtype)
+        result = thin(image).astype(bool)
         expected = np.array(
             [
                 [0, 0, 0, 0, 0, 0, 0],
@@ -162,12 +168,12 @@ def test_noiter(self):
                 [0, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 0, 0, 0, 0],
             ],
-            dtype=np.uint8,
+            dtype=bool,
         )
         assert_array_equal(result, expected)
 
     def test_baddim(self):
-        for ii in [np.zeros(3), np.zeros((3, 3, 3))]:
+        for ii in [np.zeros(3, dtype=bool), np.zeros((3, 3, 3), dtype=bool)]:
             with pytest.raises(ValueError):
                 thin(ii)
 
@@ -189,12 +195,13 @@ def test_00_01_zeros_masked(self):
         result = medial_axis(np.zeros((10, 10), bool), np.zeros((10, 10), bool))
         assert np.all(result == False)
 
-    def test_vertical_line(self):
+    @pytest.mark.parametrize("dtype", [bool, float, int])
+    def test_vertical_line(self, dtype):
         '''Test a thick vertical line, issue #3861'''
-        img = np.zeros((9, 9))
+        img = np.zeros((9, 9), dtype=dtype)
         img[:, 2] = 1
-        img[:, 3] = 1
-        img[:, 4] = 1
+        img[:, 3] = 2
+        img[:, 4] = 3
 
         expected = np.full(img.shape, False)
         expected[:, 3] = True
diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py
--- a/skimage/morphology/tests/test_skeletonize_3d.py
+++ b/skimage/morphology/tests/test_skeletonize_3d.py
@@ -1,9 +1,9 @@
 import numpy as np
+import pytest
 import scipy.ndimage as ndi
 
 from skimage import io, draw
 from skimage.data import binary_blobs
-from skimage.util import img_as_ubyte
 from skimage.morphology import skeletonize, skeletonize_3d
 
 from skimage._shared import testing
@@ -13,52 +13,52 @@
 
 
 def test_skeletonize_wrong_dim():
-    im = np.zeros(5, dtype=np.uint8)
+    im = np.zeros(5, dtype=bool)
     with testing.raises(ValueError):
         skeletonize(im, method='lee')
 
-    im = np.zeros((5, 5, 5, 5), dtype=np.uint8)
+    im = np.zeros((5, 5, 5, 5), dtype=bool)
     with testing.raises(ValueError):
         skeletonize(im, method='lee')
 
 
 def test_skeletonize_1D_old_api():
     # a corner case of an image of a shape(1, N)
-    im = np.ones((5, 1), dtype=np.uint8)
+    im = np.ones((5, 1), dtype=bool)
     res = skeletonize_3d(im)
     assert_equal(res, im)
 
 
 def test_skeletonize_1D():
     # a corner case of an image of a shape(1, N)
-    im = np.ones((5, 1), dtype=np.uint8)
+    im = np.ones((5, 1), dtype=bool)
     res = skeletonize(im, method='lee')
     assert_equal(res, im)
 
 
 def test_skeletonize_no_foreground():
-    im = np.zeros((5, 5), dtype=np.uint8)
+    im = np.zeros((5, 5), dtype=bool)
     result = skeletonize(im, method='lee')
     assert_equal(result, im)
 
 
 def test_skeletonize_all_foreground():
-    im = np.ones((3, 4), dtype=np.uint8)
+    im = np.ones((3, 4), dtype=bool)
     assert_equal(
         skeletonize(im, method='lee'),
-        np.array([[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], dtype=np.uint8),
+        np.array([[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], dtype=bool),
     )
 
 
 def test_skeletonize_single_point():
-    im = np.zeros((5, 5), dtype=np.uint8)
+    im = np.zeros((5, 5), dtype=bool)
     im[3, 3] = 1
     result = skeletonize(im, method='lee')
     assert_equal(result, im)
 
 
 def test_skeletonize_already_thinned():
-    im = np.zeros((5, 5), dtype=np.uint8)
+    im = np.zeros((5, 5), dtype=bool)
     im[3, 1:-1] = 1
     im[2, -1] = 1
     im[4, 0] = 1
@@ -74,29 +74,19 @@ def test_dtype_conv():
 
     orig = img.copy()
     res = skeletonize(img, method='lee')
-    img_max = img_as_ubyte(img).max()
 
-    assert_equal(res.dtype, np.uint8)
+    assert_equal(res.dtype, bool)
     assert_equal(img, orig)  # operation does not clobber the original
-    assert_equal(res.max(), img_max)  # the intensity range is preserved
 
 
-@parametrize("img", [np.ones((8, 8), dtype=float), np.ones((4, 8, 8), dtype=float)])
+@parametrize("img", [np.ones((8, 8), dtype=bool), np.ones((4, 8, 8), dtype=bool)])
 def test_input_with_warning(img):
     # check that the input is not clobbered
     # for 2D and 3D images of varying dtypes
     check_input(img)
 
 
-@parametrize(
-    "img",
-    [
-        np.ones((8, 8), dtype=np.uint8),
-        np.ones((4, 8, 8), dtype=np.uint8),
-        np.ones((8, 8), dtype=bool),
-        np.ones((4, 8, 8), dtype=bool),
-    ],
-)
+@parametrize("img", [np.ones((8, 8), dtype=bool), np.ones((4, 8, 8), dtype=bool)])
 def test_input_without_warning(img):
     # check that the input is not clobbered
     # for 2D and 3D images of varying dtypes
@@ -109,22 +99,23 @@ def check_input(img):
     assert_equal(img, orig)
 
 
-def test_skeletonize_num_neighbors():
+@pytest.mark.parametrize("dtype", [bool, float, int])
+def test_skeletonize_num_neighbors(dtype):
     # an empty image
-    image = np.zeros((300, 300))
+    image = np.zeros((300, 300), dtype=dtype)
 
     # foreground object 1
     image[10:-10, 10:100] = 1
-    image[-100:-10, 10:-10] = 1
-    image[10:-10, -100:-10] = 1
+    image[-100:-10, 10:-10] = 2
+    image[10:-10, -100:-10] = 3
 
     # foreground object 2
     rs, cs = draw.line(250, 150, 10, 280)
     for i in range(10):
-        image[rs + i, cs] = 1
+        image[rs + i, cs] = 4
     rs, cs = draw.line(10, 150, 250, 280)
     for i in range(20):
-        image[rs + i, cs] = 1
+        image[rs + i, cs] = 5
 
     # foreground object 3
     ir, ic = np.indices(image.shape)
@@ -132,7 +123,7 @@ def test_skeletonize_num_neighbors():
     circle2 = (ic - 135) ** 2 + (ir - 150) ** 2 < 20**2
     image[circle1] = 1
     image[circle2] = 0
-    result = skeletonize(image, method='lee')
+    result = skeletonize(image, method='lee').astype(np.uint8)
 
     # there should never be a 2x2 block of foreground pixels in a skeleton
     mask = np.array([[1, 1], [1, 1]], np.uint8)
@@ -159,7 +150,7 @@ def test_two_hole_image():
             [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         ],
-        dtype=np.uint8,
+        dtype=bool,
     )
     img_f = np.array(
         [
@@ -178,7 +169,7 @@ def test_two_hole_image():
             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         ],
-        dtype=np.uint8,
+        dtype=bool,
     )
     res = skeletonize(img_o, method='lee')
     assert_equal(res, img_f)
@@ -189,8 +180,7 @@ def test_3d_vs_fiji():
     # the skeleton generated by FIJI (Plugins>Skeleton->Skeletonize)
     img = binary_blobs(32, 0.05, n_dim=3, rng=1234)
     img = img[:-2, ...]
-    img = img.astype(np.uint8) * 255
 
     img_s = skeletonize(img)
-    img_f = io.imread(fetch("data/_blobs_3d_fiji_skeleton.tif"))
+    img_f = io.imread(fetch("data/_blobs_3d_fiji_skeleton.tif")).astype(bool)
     assert_equal(img_s, img_f)