Skip to content

Image Processing Cheat Sheet

A dense reference for classical image processing with OpenCV and scikit-image: point operations, histograms, thresholds, filters, edges, frequencies, morphology, contours, and warps, each with the call to use and when to use it.

Related chapters: Point operations and histograms · Convolution and filtering · Edges and gradients · The frequency domain · Morphology and classical segmentation · Geometric transformations

import cv2
import numpy as np
from skimage import exposure, filters, feature, morphology, measure, segmentation, transform, restoration

Conventions that cause most bugs

Topic OpenCV scikit-image / NumPy
Channel order BGR from cv2.imread RGB
Pixel type Usually uint8 in 0–255 Usually float in [0, 1] (most functions convert; preserve_range=True keeps values)
Points (x, y) = (column, row) [row, col] for indexing; (row, col) for most coordinates
Sizes (dsize) (width, height) shape = (height, width)
Filtering filter2D is correlation ndimage.correlate vs ndimage.convolve
Default border BORDER_REFLECT_101 (dcb|abcd|cba) ndimage: "reflect" (cba|abcd|dcb); "mirror" = OpenCV's default
Foreground Nonzero True
Hue range (8-bit) 0–179 0–1 (color.rgb2hsv)
Component count connectedComponents includes background label 0 measure.label(...).max() = number of objects

uint8 arithmetic wraps around: use cv2.add, cv2.subtract, or np.clip(x.astype(np.int16) + k, 0, 255).astype(np.uint8). Keep derivative and sharpening outputs in float: ddepth=cv2.CV_64F.

Point operations

Operation Formula OpenCV scikit-image When to use
Brightness/contrast \(g = \alpha f + \beta\) cv2.convertScaleAbs(img, alpha=a, beta=b) exposure.rescale_intensity Simple global adjustment; watch clipping
Percentile stretch map \([p_2, p_{98}] \to [0, 255]\) np.percentile + cv2.normalize or a LUT exposure.rescale_intensity(img, in_range=(p2, p98)) Default display enhancement; robust to outliers
Gamma \(g = 255\,(f/255)^\gamma\) cv2.LUT(img, lut) exposure.adjust_gamma(img, gamma) Brighten shadows (\(\gamma < 1\)) or darken (\(\gamma > 1\)) without clipping
Lookup table lut[img] cv2.LUT(img, lut) lut[img] Any per-pixel curve on 8-bit data; compose curves as B[A]
Invert \(255 - f\) cv2.bitwise_not(img) util.invert(img) Dark objects on light backgrounds

Histograms

Task OpenCV scikit-image / NumPy Notes
Histogram cv2.calcHist([img], [0], None, [256], [0, 256]) exposure.histogram(img, source_range="dtype"), np.bincount(img.ravel(), minlength=256) skimage defaults to the image's min-max range
CDF np.cumsum(hist) / hist.sum() exposure.cumulative_distribution(img) The equalizing transform
Global equalization cv2.equalizeHist(img) exposure.equalize_hist(img) Very flat images; amplifies noise
CLAHE cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(img) exposure.equalize_adapthist(img, clip_limit=0.01) Uneven lighting, medical, underwater; the default choice
Match a reference — exposure.match_histograms(img, ref) Normalizing a batch to one look
Color images Equalize L of Lab or Y of YCrCb Same Never equalize R, G, B separately

Thresholding

Method OpenCV scikit-image When to use
Fixed cv2.threshold(img, t, 255, cv2.THRESH_BINARY) img > t Controlled lighting only
Otsu cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) filters.threshold_otsu(img) Bimodal histogram, even lighting
Multi-Otsu — filters.threshold_multiotsu(img, classes=3) Three or more intensity classes
Triangle cv2.THRESH_TRIANGLE filters.threshold_triangle(img) Small objects, one dominant peak
Adaptive mean/Gaussian cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block, C) filters.threshold_local(img, block_size, offset=C) Uneven lighting, documents
Sauvola — filters.threshold_sauvola(img, window_size=25) Documents with stains or faint text
Color range cv2.inRange(hsv, lo, hi) color.rgb2hsv + comparisons Objects that differ by hue; red needs two ranges

Use THRESH_BINARY_INV (or ~mask) when the objects are darker than the background. Otsu: \(\sigma_B^2(t) = \omega_0\omega_1(\mu_0 - \mu_1)^2\), maximized over \(t\). Adaptive: block a few times the stroke width, \(C\) a few times the noise level.

Filters

Filter OpenCV scikit-image / SciPy When to use
Any kernel (correlation) cv2.filter2D(img, cv2.CV_64F, k) ndimage.correlate(img, k) Custom kernels; flip k for convolution
Separable kernel cv2.sepFilter2D(img, -1, kx, ky) two ndimage.correlate1d calls Rank-1 kernels: \(2K\) instead of \(K^2\) per pixel
Box (mean) cv2.blur(img, (k, k)) ndimage.uniform_filter(img, k) Fast local means; cost independent of size
Gaussian cv2.GaussianBlur(img, (0, 0), sigma) filters.gaussian(img, sigma) Default smoothing, pre-blur before edges or downsampling
Median cv2.medianBlur(img, k) filters.median(img, morphology.disk(r)) Salt-and-pepper noise; keeps edges
Bilateral cv2.bilateralFilter(img, d, sigmaColor, sigmaSpace) restoration.denoise_bilateral Smooth while keeping edges
Non-local means cv2.fastNlMeansDenoising(img, None, h) restoration.denoise_nl_means Best classical Gaussian-noise removal
Unsharp mask cv2.addWeighted(img, 1 + a, blur, -a, 0) filters.unsharp_mask(img, radius, amount) Sharpening; too much creates halos
FFT convolution cv2.filter2D (automatic for large kernels) scipy.signal.fftconvolve Kernels larger than about 15 × 15

Common kernels

Kernel Values Sum Separable? Purpose
Box 3 × 3 \(\frac{1}{9}\begin{smallmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{smallmatrix}\) 1 Yes: \(\frac13[1, 1, 1]\) Mean blur
Binomial 3 × 3 \(\frac{1}{16}\begin{smallmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{smallmatrix}\) 1 Yes: \(\frac14[1, 2, 1]\) Small Gaussian
Pyramid 5-tap \(\frac{1}{16}[1, 4, 6, 4, 1]\) (1D) 1 Yes pyrDown/pyrUp smoothing
Central difference \(\frac12[-1, 0, 1]\) 0 1D Derivative (correlation form)
Sobel \(x\) \(\begin{smallmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{smallmatrix}\) 0 Yes: \([1, 2, 1]^\top[-1, 0, 1]\) Gradient, gain 8
Scharr \(x\) \(\begin{smallmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \end{smallmatrix}\) 0 Yes Accurate gradient direction
Laplacian \(\begin{smallmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{smallmatrix}\) 0 No (rank 2) Second derivative, zero crossings
Sharpen \(\begin{smallmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{smallmatrix}\) 1 No Identity minus Laplacian
Gaussian \(\frac{1}{2\pi\sigma^2}e^{-(u^2 + v^2)/2\sigma^2}\), radius \(\approx 3\sigma\) 1 Yes Isotropic blur; \(\sigma^2\) adds when composed

Blur kernels sum to 1 (brightness unchanged); derivative kernels sum to 0 (flat regions give 0).

Edges

Task OpenCV scikit-image Notes
Derivatives cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3), cv2.Scharr(...) filters.sobel_v, filters.sobel_h (divided by 4), filters.scharr Keep float output
Magnitude, direction cv2.cartToPolar(gx, gy, angleInDegrees=True) np.hypot(gx, gy), np.arctan2(gy, gx) Direction is perpendicular to the edge
Laplacian cv2.Laplacian(img, cv2.CV_64F) filters.laplace Very noise-sensitive
LoG GaussianBlur + Laplacian ndimage.gaussian_laplace(img, sigma) Blob and zero-crossing detection
DoG difference of two GaussianBlurs filters.difference_of_gaussians(img, s1, s2) Cheap LoG; band-pass
Canny cv2.Canny(blurred, low, high, L2gradient=True) feature.canny(img, sigma, low, high) OpenCV doesn't blur; ratio high:low of 2–3

Canny = smooth, gradient, non-maximum suppression, double threshold, hysteresis. Thresholds are in gradient units (Sobel gain 8), so they depend on contrast; use_quantiles=True in scikit-image is the most robust.

Frequency domain

Task Call Notes
2D FFT / inverse F = np.fft.fft2(img), np.fft.ifft2(F).real Unnormalized forward, \(1/HW\) inverse
Real-input FFT np.fft.rfft2, np.fft.irfft2 Half the work for real images
Center the spectrum np.fft.fftshift(F), np.fft.ifftshift Frequency 0 to the middle
Display np.log1p(np.abs(np.fft.fftshift(F))) Log scale, always
Frequencies np.fft.fftfreq(n) Cycles per pixel, Nyquist = 0.5
Convolution theorem ifft2(fft2(f) * fft2(h_padded)) Circular; pad for linear, or signal.fftconvolve
Gaussian low-pass \(H = e^{-D^2/2D_0^2}\) Equals spatial \(\sigma = N/(2\pi D_0)\); no ringing
High-pass \(1 - H_{\text{low}}\) Edges and texture
Notch Multiply small holes at interference peaks Periodic noise
Registration cv2.phaseCorrelate(a, b), registration.phase_cross_correlation Translation from the phase
Downsample safely cv2.resize(img, dsize, interpolation=cv2.INTER_AREA), transform.resize(..., anti_aliasing=True) Low-pass before subsampling
Gaussian pyramid cv2.pyrDown(img) 5-tap blur + drop every other pixel
Laplacian pyramid L = G - cv2.pyrUp(cv2.pyrDown(G), dstsize=G.shape[::-1]) Exactly invertible; blending, compression

Morphology

Operation OpenCV scikit-image What it does
Structuring element cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) morphology.disk(r), morphology.footprint_rectangle((k, k)) Shape and scale of the probe
Erosion cv2.erode(m, se) morphology.erosion / binary_erosion Shrink; remove thin parts (min filter)
Dilation cv2.dilate(m, se) morphology.dilation Grow; fill small gaps (max filter)
Opening cv2.morphologyEx(m, cv2.MORPH_OPEN, se) morphology.opening Remove specks and spurs
Closing cv2.MORPH_CLOSE morphology.closing Fill holes and gaps
Gradient cv2.MORPH_GRADIENT dilation - erosion Outlines
White / black top-hat cv2.MORPH_TOPHAT / cv2.MORPH_BLACKHAT morphology.white_tophat / black_tophat Remove uneven background
Fill holes ndimage.binary_fill_holes(m) morphology.remove_small_holes Solid objects
Remove small objects filter connectedComponentsWithStats by area morphology.remove_small_objects(m, max_size=n) Drop noise blobs
Reconstruction — morphology.reconstruction(seed, mask) Opening that keeps exact shapes
Skeleton — morphology.skeletonize(m) One-pixel-wide medial lines

Components, contours, and segmentation

Task OpenCV scikit-image Notes
Label components n, labels, stats, cents = cv2.connectedComponentsWithStats(m, connectivity=8) measure.label(m, connectivity=2) OpenCV's n includes the background
Region properties stats[:, cv2.CC_STAT_AREA] measure.regionprops_table(labels, img, properties=(...)) Area, centroid, eccentricity, solidity...
Contours cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) measure.find_contours(img, level) OpenCV points are (x, y); skimage (row, col)
Area, perimeter cv2.contourArea(c), cv2.arcLength(c, True) regionprops: area, perimeter Circularity \(4\pi A/P^2\)
Polygon simplification cv2.approxPolyDP(c, 0.02 * peri, True) measure.approximate_polygon(c, tol) Finds corners (quadrilaterals)
Hull, boxes, ellipse cv2.convexHull, cv2.boundingRect, cv2.minAreaRect, cv2.fitEllipse regionprops: area_convex, bbox, orientation Solidity = area / hull area
Moments cv2.moments(c), cv2.HuMoments(m) measure.moments, measure.moments_hu Centroid \(= (m_{10}/m_{00}, m_{01}/m_{00})\)
Distance transform cv2.distanceTransform(m, cv2.DIST_L2, 5) ndimage.distance_transform_edt(m) Peaks at object centers
Watershed cv2.watershed(bgr, markers_int32) segmentation.watershed(-dist, markers, mask=m) Always use markers
Markers threshold the distance feature.peak_local_max(dist, min_distance=d, labels=lab) Local maxima adapt to object size
Region growing cv2.floodFill segmentation.flood(img, seed, tolerance=t) Magic wand
k-means colors cv2.kmeans(X, k, None, criteria, attempts, cv2.KMEANS_PP_CENTERS) sklearn.cluster.KMeans Cluster in Lab
Superpixels — segmentation.slic(img, n_segments) Compact color regions

Geometric transforms

Task OpenCV scikit-image Notes
Resize cv2.resize(img, (w, h), interpolation=...) transform.resize(img, (h, w)) INTER_AREA to shrink, INTER_CUBIC to enlarge
Rotation matrix cv2.getRotationMatrix2D((cx, cy), deg, scale) transform.rotate(img, deg) OpenCV: positive = counterclockwise
Affine (3 pairs) cv2.getAffineTransform(src, dst) transform.AffineTransform.from_estimate(src, dst) 6 DOF; keeps parallel lines
Homography (4 pairs) cv2.getPerspectiveTransform(src, dst) transform.ProjectiveTransform.from_estimate(src, dst) 8 DOF; keeps straight lines
Homography (many, outliers) cv2.findHomography(src, dst, cv2.RANSAC, 3.0) measure.ransac Level 2
Warp an image cv2.warpAffine(img, M, (w, h)), cv2.warpPerspective(img, H, (w, h)) transform.warp(img, inverse_map) skimage wants the inverse
Warp points cv2.perspectiveTransform(pts.reshape(-1, 1, 2), H), cv2.transform tform(pts) Divide by \(w\) for homographies
Arbitrary map cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR) transform.warp(img, coords) Inverse mapping
Interpolation OpenCV flag Use for
Nearest INTER_NEAREST Masks and label maps
Bilinear INTER_LINEAR Default
Bicubic INTER_CUBIC Quality enlargement (may overshoot; clip)
Lanczos INTER_LANCZOS4 Highest-quality enlargement
Area INTER_AREA Shrinking (anti-aliased)

Compose matrices and resample once. Order rectification corners top-left, top-right, bottom-right, bottom-left. Homogeneous: point \((x, y, 1)\), line \((a, b, c)\); the line through two points and the intersection of two lines are both cross products.