← All insights

Machine vision · Python · Test methodology

OpenCV with Python: comparing Otsu, adaptive and fixed thresholds

Which threshold fits the inspection task? A fixed threshold, Otsu and adaptive thresholding do not solve the same problem. This guide provides a traceable comparison using Python and OpenCV: from the image region to a binary mask and the questions that must be answered before industrial use.

The example is a learning and comparison prototype for dark features on a bright background. It contains no customer images, production metrics or release claim. The selection and test recommendations are an engineering approach, not universal acceptance limits.

Fixed threshold, Otsu or adaptive?

A fixed threshold uses the same specified value everywhere. Otsu derives a global value from the histogram; well-separated intensity groups are a useful starting point. Adaptive methods calculate local thresholds instead. The OpenCV tutorial [1] explains these foundations.

Comparing three methods
Starting conditionFirst candidateCritical countercheck
Contrast remains stable across the inspection regionFixed thresholdDoes the same value survive permitted brightness changes?
Two intensity groups in the selected regionOtsuDoes changing the object proportion still yield a useful result?
Background brightness varies spatiallyAdaptiveAre entire regions detected, or only their edges?

My recommendation is to compare all three candidates on the same representative images first. A more elaborate method does not automatically win. If a simple method satisfies the requirements defined beforehand, another parameter layer initially introduces another potential source of error.

Define the region of interest (ROI) from the task before tuning. A changing crop is a different input and must be documented as such. Tuning an algorithm on a tight crop and later processing the entire camera image is not a comparison under identical conditions.

Python example: three masks with identical preprocessing

You need Python, NumPy and OpenCV; this article uses version 4.12 as its API reference. The script expects your own file named inspection.png: a single-channel 8-bit image measuring at least 31 × 31 pixels. It explicitly rejects colour images and higher-bit-depth intensity values. IMREAD_UNCHANGED avoids silently converting the input during loading here. See the file API [5].

import cv2 as cv
import numpy as np


def compare_thresholds(gray):
    if gray is None or not isinstance(gray, np.ndarray):
        raise ValueError("Provide a readable image.")
    if gray.ndim != 2 or gray.dtype != np.uint8:
        raise ValueError("Expected single-channel uint8.")
    if min(gray.shape) < 31:
        raise ValueError("Example requires at least 31 x 31 pixels.")

    smooth = cv.GaussianBlur(gray, (3, 3), 0)
    _, fixed = cv.threshold(
        smooth, 120, 255, cv.THRESH_BINARY_INV
    )
    otsu_value, otsu = cv.threshold(
        smooth, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU
    )
    adaptive = cv.adaptiveThreshold(
        smooth, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv.THRESH_BINARY_INV, 31, 7
    )
    return otsu_value, {
        "fixed": fixed, "otsu": otsu, "adaptive": adaptive
    }


if __name__ == "__main__":
    gray = cv.imread("inspection.png", cv.IMREAD_UNCHANGED)
    threshold, masks = compare_thresholds(gray)
    print("OpenCV:", cv.__version__)
    print("Otsu threshold:", threshold)
    for name, mask in masks.items():
        print(name, mask.shape, mask.dtype)

The masks stay in memory; input files are not overwritten. Every method receives the same smoothed input. With THRESH_BINARY_INV, values up to and including the respective threshold become white. In this example the dark inspection feature is therefore the white foreground. The reported Otsu value is an intensity threshold, not a confidence score or quality measure. API details [2].

The numbers 120, 31 and 7 are starting values for comparison, not industrial recommendations. A synthetic functional test can demonstrate working formats and calls; it cannot replace evaluation on real inspection images. Record the actual installed library versions in the test log to support reproduction.

Vary parameters deliberately, not all at once

Window size and offset

adaptiveThreshold() requires a single-channel 8-bit image and an odd window size greater than one. The Gaussian method subtracts C from the locally weighted mean. OpenCV reference [2].

A useful limiting case follows directly: if the window lies entirely inside a uniformly dark region and C is positive, its pixel value is above “local mean minus C”. The inverted mask becomes black there. A region can therefore disappear internally even though its edges are detected. Adaptive thresholding is not synonymous with complete object segmentation.

A useful countercheck tests the same feature size at several image positions and with several window sizes. Initially keep everything else constant. When image resolution changes, an unchanged window size in pixels must not be assumed to represent the same application scale without checking.

Smoothing and morphology

Gaussian smoothing reduces high-frequency image content but can also blur edges. The example's 3 × 3 smoothing is therefore only a comparison condition. An additional test without smoothing helps reveal whether small relevant details disappear. OpenCV on filters [3].

Opening consists of erosion followed by dilation; closing reverses that sequence. These can remove small foreground structures or close small holes, respectively. OpenCV on morphology [4]. Those very structures could be the defects being sought. The example therefore deliberately includes no automatic “cleanup”.

Before postprocessing, record what counts as nuisance and what counts as a defect. If a dark scratch is represented as a white feature, cleanup must not delete it merely because it is small. Keep the raw and processed masks side by side during testing.

A test plan beyond attractive sample images

For a defensible comparison, I recommend the following sequence. It is independent of which candidate looks best on the first image:

  1. Define the decision: Is the task to locate a part, detect a defect or measure an area? A similar-looking mask is not yet a correct inspection decision.
  2. Establish references: Keep the original image, expected result and reasoning together. Mark ambiguous cases separately instead of relabelling them to fit the result afterwards.
  3. Separate tuning and evaluation data: Set parameters on one subset, then evaluate on previously unused images. Do not treat nearly identical images of the same part as independent confirmation.
  4. Include boundary cases deliberately: Cover permitted variations in position, material and brightness, empty images, borderline cases, and both defective and acceptable parts. The application determines which variations are permitted.
  5. Count errors separately: Record missed defects separately from falsely rejected acceptable parts. For measurement tasks, also measure against a suitable reference; the pixel mask alone is insufficient.
  6. Make changes traceable: Version the dataset, ROI, filters, thresholding method, parameters and software together. Test runtime on the intended target system with the complete processing chain.

One possible record is: image ID → reference decision → configuration → result → deviation → open question. This proposal does not supply acceptance limits. Derive those from the risks and requirements of the specific task before the final evaluation.

Geometric measurements also depend on imaging geometry: the OpenCV camera calibration guide covers that separate building block. The article on knowledge graphs in machine vision explains how configurations and evidence can be connected.

Frequently asked questions

Why does Otsu not automatically produce the correct mask?

Otsu does not know what a defect means. A histogram contains intensity frequencies, not application labels such as “acceptable” or “defective”. The computed result must therefore be checked against the intended inspection decision.

Should a 16-bit camera image simply be converted to uint8?

Not silently. The example intentionally rejects it. Any required scaling belongs in the documented processing chain, including input range and information loss; parameters must be checked again after a change.

When is a different approach needed?

If relevant cases cannot be separated reliably within permitted operating conditions, reassess the task: acquisition conditions, suitable features or a different segmentation method. One more parameter alone is not evidence that the cause has been addressed.

Primary sources

  1. OpenCV 4.12: Image Thresholding
  2. OpenCV 4.12: threshold / adaptiveThreshold API
  3. OpenCV 4.12: Smoothing Images
  4. OpenCV 4.12: Morphological Transformations
  5. OpenCV 4.12: Image File Reading and Writing

Documentation baseline: OpenCV 4.12; sources checked on 11 September 2026. The code is an original comparison example. The test strategy and the interpretation of uniform region interiors are engineering recommendations derived from these foundations.