mrkeyoor.com_
Wed 05 Aug 19:54 UTC
PyPIAI / MLupdated 05 Aug 2026

scikit-image

scikit-image (imported as skimage) is the scientific Python ecosystem's image processing library: classical algorithms like filtering, thresholding, segmentation, feature detection, morphology, and measurement, all operating on plain NumPy arrays. It is the sibling of scipy and scikit-learn in style: well-documented functions with citations to the papers they implement, aimed at analysis and measurement (microscopy, medical, satellite imagery) rather than at real-time vision or deep learning.

Verdict

The best-documented classical image processing library in Python and the right default for scientific measurement work on NumPy arrays. Know what it is not: not fast enough for real-time, not GPU-aware, and not a deep learning tool, so pair it accordingly.

API stability4/5Still 0.x with a formal deprecation policy: changes like selem to footprint and multichannel to channel_axis went through warning cycles, so code rots slowly and loudly rather than breaking overnight
Docs5/5Reference docs with literature citations, a large runnable example gallery, and an active user forum at forum.image.sc; among the best documentation in scientific Python
Maintenance4/5Pushed the day before this review with steady releases; community-governed under Scientific Python SPECs rather than corporate-backed, with about 933 open issues and PRs and no full-time team
Ecosystem4/5Deeply woven into the NumPy/scipy stack and the standard teaching library for image analysis, though its plugin and tooling world is smaller than OpenCV's and it has no deep learning bridge

Use it if

  • You are measuring things in images: label connected regions with measure.label and pull area, centroid, and eccentricity per object via regionprops
  • You need classical algorithms with documented provenance (Otsu thresholding, watershed, Canny, CLAHE) and a gallery example to start from
  • Your images are already NumPy arrays in a scipy/pandas/matplotlib workflow, and you want functions that compose with that stack instead of a separate framework
  • You work with scientific formats and channels beyond RGB photos: 16-bit TIFF stacks, 3D volumes, and float images are first-class here
Skip it if

Setup reality

pip install scikit-image ships wheels for all major platforms, so installs are painless, but it drags in scipy, networkx, pillow, imageio, tifffile, and lazy-loader, which is a chunky tree if you only wanted one filter. Building from source is a different story: meson, ninja, Cython, and pythran are required, so stick to wheels or conda-forge. The recurring runtime surprise is dtype convention: functions assume float images live in [0, 1], and mixing uint8 [0, 255] arrays with float outputs without img_as_float produces silently wrong math rather than errors.

Patterns

Load an image and normalize dtyperead-image-as-float

from skimage import io, util

img = io.imread("scan.png")        # usually uint8, [0, 255]
imgf = util.img_as_float(img)       # float64, [0, 1]

Most skimage functions assume float images sit in [0, 1]. Passing raw uint8 into math that expects floats gives wrong results silently, so convert at the boundary and stay float inside.

Convert RGB to grayscalergb-to-grayscale

from skimage.color import rgb2gray

gray = rgb2gray(img)  # float in [0, 1], shape (H, W)

rgb2gray rejects RGBA; strip the alpha channel first with rgba2rgb or img[..., :3]. The output is always float regardless of input dtype.

Automatic thresholding with Otsuotsu-threshold

from skimage.filters import threshold_otsu

t = threshold_otsu(gray)
mask = gray > t  # boolean foreground mask

Otsu assumes a bimodal histogram; on uneven illumination it fails badly, and threshold_local (adaptive) is the fix. The function returns the threshold value, not the mask.

Count and measure objects in a masklabel-and-measure-regions

from skimage import measure

labels = measure.label(mask)
props = measure.regionprops_table(
    labels, properties=("label", "area", "centroid", "eccentricity"))
import pandas as pd
df = pd.DataFrame(props)

regionprops_table returns plain dict-of-arrays ready for pandas, which beats looping over regionprops objects. Remember label 0 is background and is excluded.

Resize with proper anti-aliasingresize-rescale

from skimage.transform import resize, rescale

small = resize(img, (256, 256), anti_aliasing=True)
half = rescale(img, 0.5, anti_aliasing=True, channel_axis=-1)

Output is float in [0, 1] even for uint8 input; use img_as_ubyte to go back. For multichannel images pass channel_axis=-1 or the channels get interpolated as a spatial dimension.

Canny edge detectionedge-detection-canny

from skimage.feature import canny

edges = canny(gray, sigma=2.0)  # boolean edge map

sigma is the knob that matters: low values keep noise as edges, high values drop fine detail. Canny lives in skimage.feature, not skimage.filters, which trips people up.

Clean a binary mask with morphologymorphology-cleanup

from skimage.morphology import binary_opening, remove_small_objects, disk

clean = binary_opening(mask, footprint=disk(3))
clean = remove_small_objects(clean, min_size=64)

The structuring element argument is footprint; old tutorials say selem, which is the removed pre-0.19 name. remove_small_objects wants a boolean array, not 0/255 uint8.

Split touching objects with watershedwatershed-split-touching

import numpy as np
from scipy import ndimage as ndi
from skimage.feature import peak_local_max
from skimage.segmentation import watershed

dist = ndi.distance_transform_edt(mask)
coords = peak_local_max(dist, footprint=np.ones((3, 3)), labels=mask)
markers = np.zeros(dist.shape, dtype=int)
markers[tuple(coords.T)] = np.arange(1, len(coords) + 1)
labels = watershed(-dist, markers, mask=mask)

Watershed floods from markers over the negated distance map; bad markers mean over-segmentation. peak_local_max returns coordinates now, not a boolean image as in very old examples.

Denoise while keeping edgesdenoise

from skimage.restoration import denoise_tv_chambolle

smooth = denoise_tv_chambolle(imgf, weight=0.1, channel_axis=-1)

Total-variation denoising preserves edges better than a Gaussian blur but flattens texture; tune weight down if results look like posterized paint. Input should be float.

Adaptive histogram equalization (CLAHE)clahe-contrast

from skimage import exposure

better = exposure.equalize_adapthist(gray, clip_limit=0.02)

equalize_adapthist internally works on floats and returns [0, 1]. It amplifies noise in flat regions, so denoise first if the input is grainy.

Find a template inside an imagetemplate-matching

import numpy as np
from skimage.feature import match_template

result = match_template(gray, template)
y, x = np.unravel_index(np.argmax(result), result.shape)

The peak gives the template's top-left corner, not its center. Correlation is not scale or rotation invariant; if the object varies in size, this is the wrong tool.

Alternatives

PackageRegistryPick it when
opencv-pythonPyPISpeed, video capture, or real-time pipelines matter more than API clarity
pillowPyPIYou only need loading, resizing, cropping, and format conversion
korniaPyPIYou want image operations on GPU tensors inside a PyTorch pipeline
torchvisionPyPIThe task is deep learning on images rather than classical analysis