Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Decision matrix for Global vs Otsu vs Adaptive thresholding, CLAHE parameter tuning, and histogram-based preprocessing
.claude/skills/aeren23-thresholding-strategy/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 7% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 65% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 107% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 134% | 0% |
Image lighting conditions?
├── Uniform lighting, good contrast
│ ├── You know the ideal threshold value
│ │ └── ✅ Global Threshold (cv2.threshold)
│ └── You don't know the ideal value
│ └── ✅ Otsu's Method (automatic optimal threshold)
│
├── Uneven lighting / shadows present
│ └── ✅ Adaptive Threshold (cv2.adaptiveThreshold)
│ ├── General use → ADAPTIVE_THRESH_MEAN_C
│ └── Text/document → ADAPTIVE_THRESH_GAUSSIAN_C (better)
│
└── Very low contrast (details invisible)
└── First enhance contrast, THEN threshold:
├── Moderate enhancement → Histogram Stretching
├── Strong enhancement → Histogram Equalization
└── Local enhancement → CLAHE (best for most cases)| Method | Function | Effect | Best For | |--------|----------|--------|----------| | Histogram Stretching | cv2.normalize(NORM_MINMAX) | Linear rescale min→0, max→255 | Natural-looking enhancement | | Histogram Equalization | cv2.equalizeHist() | Aggressive CDF-based redistribution | Maximum contrast, looks artificial | | CLAHE | cv2.createCLAHE() | Local adaptive equalization | Medical images, preserves local detail |
> Default choice: CLAHE. It provides strong enhancement without the over-amplification artifacts of global equalization.
| Type | Above Threshold | Below Threshold | Use Case | |------|----------------|-----------------|----------| | THRESH_BINARY | White (255) | Black (0) | Standard foreground extraction | | THRESH_BINARY_INV | Black (0) | White (255) | Dark objects on light background | | THRESH_TRUNC | Clamped to threshold | Unchanged | Brightness capping | | THRESH_TOZERO | Unchanged | Set to 0 | Keep only bright regions | | THRESH_TOZERO_INV | Set to 0 | Unchanged | Keep only dark regions |
Thresholding only works on single-channel (grayscale) images.
pythongray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
Otsu minimizes intra-class variance to find the optimal split point. Pass 0 as threshold and add the flag:
python# Otsu finds optimal threshold automatically _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
python# CRASHES cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 10, 3) # blockSize=10 → Error! # CORRECT cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 3) # blockSize=11 ✓
pythonclahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8)) result = clahe.apply(gray)
| Parameter | Range | Effect | |-----------|-------|--------| | clipLimit | 2.0 - 3.0 (typical) | Higher = more contrast, but more noise amplification | | tileGridSize | (4,4) to (16,16) | Smaller tiles = more local adaptation, risk of artifacts |
> Safe defaults: clipLimit=2.5, tileGridSize=(8,8)
cv2.equalizeHist() accepts only single-channel images. For color images, convert to LAB or YCrCb, equalize the L/Y channel only, then convert back.
| Histogram Shape | Diagnosis | Action | |-----------------|-----------|--------| | Clustered on left (dark) | Underexposed image | Apply CLAHE or equalization | | Clustered on right (bright) | Overexposed image | Apply CLAHE or normalize | | Narrow peak in center | Low contrast | Histogram stretching or CLAHE | | Wide, uniform spread | Good contrast | Ready for thresholding | | Two distinct peaks (bimodal) | Clear foreground/background | Otsu will work perfectly |
pythonhist = cv2.calcHist([gray], [0], None, [256], [0, 256]) # [gray] — input image (list) # [0] — channel index # None — no mask # [256] — number of bins # [0, 256] — pixel value range
python# Method 1: Histogram Stretching (natural) stretched = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX) # Method 2: Global Equalization (aggressive) equalized = cv2.equalizeHist(gray) # Method 3: CLAHE (recommended) clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8)) enhanced = clahe.apply(gray)
python# MEAN_C: simple average of neighborhood binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 3) # GAUSSIAN_C: weighted average (better for text) binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 3) # blockSize=11: neighborhood size (must be odd) # C=3: constant subtracted from computed mean
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,022 | 11,721 | -16% | 1 | 1 | 0% | 2,546 | 3,657 | +44% | 0 | 0 | — |
case-02 | pass→pass | 6,289 | 4,109 | -35% | 1 | 1 | 0% | 1,455 | 2,402 | +65% | 0 | 0 | — |
case-03 | pass→pass | 5,454 | 4,804 | -12% | 1 | 1 | 0% | 1,271 | 2,628 | +107% | 0 | 0 | — |
case-04 | pass→pass | 4,331 | 4,165 | -4% | 1 | 1 | 0% | 1,005 | 2,354 | +134% | 0 | 0 | — |
case-05 | pass→pass | 8,083 | 5,806 | -28% | 1 | 1 | 0% | 1,903 | 2,838 | +49% | 0 | 0 | — |
case-06 | pass→pass | 5,539 | 5,331 | -4% | 1 | 1 | 0% | 1,312 | 2,705 | +106% | 0 | 0 | — |
case-07 | pass→pass | 6,359 | 6,045 | -5% | 1 | 1 | 0% | 1,494 | 2,909 | +95% | 0 | 0 | — |
case-08 | pass→pass | 9,277 | 5,262 | -43% | 1 | 1 | 0% | 2,063 | 2,661 | +29% | 0 | 0 | — |
case-09 | pass→pass | 6,863 | 6,297 | -8% | 1 | 1 | 0% | 1,613 | 2,920 | +81% | 0 | 0 | — |
case-10 | pass→pass | 6,153 | 4,688 | -24% | 1 | 1 | 0% | 1,398 | 2,596 | +86% | 0 | 0 | — |
case-11 | pass→pass | 7,352 | 5,467 | -26% | 1 | 1 | 0% | 1,337 | 2,824 | +111% | 0 | 0 | — |
case-12 | pass→pass | 4,943 | 3,980 | -19% | 1 | 1 | 0% | 1,050 | 2,196 | +109% | 0 | 0 | — |
case-13 | pass→pass | 10,347 | 9,121 | -12% | 1 | 1 | 0% | 2,109 | 3,475 | +65% | 0 | 0 | — |
case-14 | pass→pass | 9,246 | 5,849 | -37% | 1 | 1 | 0% | 2,128 | 2,890 | +36% | 0 | 0 | — |
case-15 | fail→pass | 11,293 | 5,487 | -51% | 1 | 1 | 0% | 2,582 | 2,755 | +7% | 0 | 0 | — |
case-16 | pass→pass | 2,620 | 2,085 | -20% | 1 | 1 | 0% | 503 | 1,933 | +284% | 0 | 0 | — |
case-17 | pass→pass | 6,070 | 3,779 | -38% | 1 | 1 | 0% | 1,136 | 2,328 | +105% | 0 | 0 | — |
case-18 | pass→pass | 5,134 | 4,375 | -15% | 1 | 1 | 0% | 1,265 | 2,571 | +103% | 0 | 0 | — |
case-19 | pass→pass | 4,459 | 2,741 | -39% | 1 | 1 | 0% | 942 | 2,080 | +121% | 0 | 0 | — |
case-20 | pass→pass | 3,474 | 2,967 | -15% | 1 | 1 | 0% | 965 | 2,250 | +133% | 0 | 0 | — |
case-21 | pass→pass | 5,381 | 4,779 | -11% | 1 | 1 | 0% | 1,225 | 2,470 | +102% | 0 | 0 | — |
case-22 | pass→pass | 6,958 | 7,662 | +10% | 1 | 1 | 0% | 1,674 | 3,417 | +104% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.