Image enhancement and filtering: A complete guide to Gaussian blur, median filtering, and histogram equalization

After adjusting the model late at night, the loss remains high, and edge detection frequently misses key contours. When I look back - the original image is densely packed with noise, or the contrast is so bad that even my mother can't recognize it? In the complete process of computer vision, image enhancement and filtering are often the "pre-savior": it can directly improve human readability, and can significantly improve the effects of downstream feature extraction and model training.

This article will take you through the most practical OpenCV algorithms, with reproducible code, a quick tuning table, and a 10-line code pipeline. After reading this, you will be able to easily solve 80% of daily image preprocessing problems.

📂 Stage: Stage 1 - Cornerstone of Image Processing (Traditional CV) 🔗 Related chapters: OpenCV 快速入门 · 边缘检测与轮廓提取


1. Basic preparation: all filtering ≈ sliding window operation with/without padding

Whether it is denoising, smoothing or enhancing details, the core logic behind it remains almost unchanged:

  1. Define a small matrix called "convolution kernel/template" (commonly used are 3×3, 5×5, and 7×7, and the side length is usually an odd number).
  2. Let this kernel slide through each pixel of the image (OpenCV usesBORDER_DEFAULTSupplement the edges to ensure that the output size is consistent with the original image).
  3. Calculate the pixel value in the kernel coverage area according to the rules and replace the center pixel.
📦 Simplified version without padding convolution demonstration
import numpy as np

# 3×3 锐化卷积核(常用到可以直接背下来!)
sharpen_kernel = np.array([
    [0, -1, 0],
    [-1, 5, -1],
    [0, -1, 0]
])

def mini_conv(image, kernel):
    """仅演示原理,生产环境务必用 cv2.filter2D"""
    h, w = image.shape[:2]
    k_h, k_w = kernel.shape
    out = np.zeros((h - k_h + 1, w - k_w + 1))  # 无 padding 导致尺寸缩小
    for i in range(out.shape[0]):
        for j in range(out.shape[1]):
            out[i,j] = np.sum(image[i:i+k_h, j:j+k_w] * kernel)
    return np.uint8(out)

2. Linear filtering: a good helper for dealing with uniform Gaussian noise

The output of linear filtering is a weighted linear combination of neighborhood pixels, which is fast in calculation, but at the cost of edges being blurred. Simply put, it adds up the neighbors around each pixel according to a certain weight. The noise is averaged out, but the boundaries of the object also become "softer".

2.1 Gaussian blur (⭐️ The preferred smoothing method for beginners)

Gaussian blur uses a two-dimensional Gaussian function as a weight, and the pixels closer to the center have greater influence. It evenly removes Gaussian noise (such as film grain, electronic noise in low light) while retaining more detail than a simple mean filter.

import cv2
import numpy as np

def gaussian_demo(img_path):
    img = cv2.imread(img_path)
    # 参数说明:
    # 1. 原图
    # 2. 核大小(必须是正奇数,优先用 3/5/7/9,越大越模糊)
    # 3. sigma(0 表示根据核大小自动计算)
    blur_light = cv2.GaussianBlur(img, (5,5), 0)
    blur_strong = cv2.GaussianBlur(img, (15,15), 0)
    return img, blur_light, blur_strong

2.2 Mean filtering (⚡️ only for introductory demonstration / ultra-fast batch blurring)

All neighborhood pixels have the same weight, which is the fastest to calculate, but also the most serious damage to edges. Generally it is only suitable for quickly blurring the background or unimportant areas.

def mean_demo(img):
    # 等价于 cv2.boxFilter(img, -1, (5,5), normalize=True)
    blur = cv2.blur(img, (5,5))
    return blur

3. Nonlinear filtering: intelligently retain edges and accurately remove noise

Nonlinear filtering does not follow a simple weighted summation rule. It "depends on the situation" - adaptive processing based on the color and distance differences between pixels. This way, flat areas are strongly smoothed, while near boundaries are carefully preserved.

3.1 Median filtering (😎 The exclusive nemesis of salt and pepper noise)

Replace the center pixel with the median of the neighborhood pixels. It can completely eliminate black and white salt and pepper noise (scratches in old photos, spots caused by transmission interference), and the edge preservation effect is much better than linear filtering. The principle is very simple: the gray value of salt and pepper noise is either 0 (black) or 255 (white), which is an extreme value. The median is naturally not affected by extreme values, so these noise points will be replaced by "normal" gray values ​​in the neighborhood.

def add_salt_pepper(img, amount=0.02):
    """演示用:生成带椒盐噪声的图"""
    noisy = img.copy()
    total = img.size // 3
    # 盐噪声(白点)
    num_salt = int(amount * total * 0.5)
    coords = [np.random.randint(0, i-1, num_salt) for i in img.shape[:2]]
    noisy[coords[0], coords[1], :] = 255
    # 胡椒噪声(黑点)
    num_pepper = int(amount * total * 0.5)
    coords = [np.random.randint(0, i-1, num_pepper) for i in img.shape[:2]]
    noisy[coords[0], coords[1], :] = 0
    return noisy

def median_demo(img_path):
    img = cv2.imread(img_path)
    noisy = add_salt_pepper(img)
    # 核大小只能是奇数,且要大致匹配噪声斑点的直径
    filtered = cv2.medianBlur(noisy, 5)
    return img, noisy, filtered

3.2 Bilateral filtering (✨ Portrait skin resurfacing / edge preservation artifact)

Bilateral filtering considers two factors at the same time:

  • Spatial distance: The closer it is, the greater the weight.
  • Color Distance: The closer the color, the greater the weight.

Therefore, smooth areas (face skin) can be severely blurred, while edge areas (eyebrow contours, hairline) are barely processed, achieving both denoising and detail preservation.

def bilateral_demo(img_path):
    img = cv2.imread(img_path)
    # 参数速记:
    # d        – 邻域直径(太大会拖慢计算速度)
    # sigmaColor – 颜色标准差(越大,对颜色差异越宽容)
    # sigmaSpace – 空间标准差(越大,空间范围越大)
    light = cv2.bilateralFilter(img, 9, 75, 75)   # 轻微磨皮
    heavy = cv2.bilateralFilter(img, 15, 200, 200) # 重度磨皮
    return img, light, heavy

Bilateral filtering tuning cheat sheet:

scenedsigmaColorsigmaSpace
Slight denoising + retaining hard details53030
Daily Portrait Microdermabrasion97575
Internet celebrity-level heavy microdermabrasion15200200

4. Histogram technology: accurately enhance contrast

The image histogram shows the distribution of pixel values ​​(0−255). Histogram equalization is to "stretch" the distribution that was originally concentrated in dark or bright parts to the entire grayscale range, making the image look more transparent and the details clearer.

4.1 Standard histogram equalization (simple and crude, but need to be used with caution)

It uniformly equalizes the entire picture, which is suitable for single-scene pictures that are overall dark or bright. The disadvantage is that it is easy to over-enhance local noise. Color images must first pull out the brightness channel separately and process it!

def color_global_hist_eq(img_path):
    """彩色图推荐转 YUV,只处理 Y(亮度)通道!"""
    img = cv2.imread(img_path)
    yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
    yuv[:,:,0] = cv2.equalizeHist(yuv[:,:,0])
    eq = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)
    return img, eq

4.2 CLAHE (🎯 Contrast-limited Adaptive Equalization · Ultimate Recommendation)

CLAHE first divides the image into many small grids (default 8×8), performs equalization on each grid separately, and then uses bilinear interpolation to smoothly splice them. Use at the same timeclipLimitLimit the improvement of local contrast, perfectly avoid over-enhancement, and all details in dark and bright parts can be clearly presented.

def color_clahe(img_path):
    """彩色图推荐用 LAB 空间(亮度 L 与颜色 A/B 分离更好,伪彩更少)"""
    img = cv2.imread(img_path)
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
    # 参数:clipLimit(默认 2.0,越大对比度越强,但噪声也可能更明显)
    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
    lab[:,:,0] = clahe.apply(lab[:,:,0])
    eq = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
    return img, eq

5. Practical combat: Universal image enhancement pipeline with 10 lines of code

Encapsulate commonly used enhancement and filtering methods into a class, use chain calls to quickly match and combine, and flexibly adapt to various daily preprocessing needs.

class EasyEnhancer:
    def __init__(self, img_path):
        self.orig = cv2.imread(img_path)
        if self.orig is None: raise ValueError("图像读取失败,请检查路径")
        self.img = self.orig.copy()
    
    # 所有方法均支持链式调用
    def gaussian(self, k=5): 
        self.img = cv2.GaussianBlur(self.img, (k,k), 0)
        return self
    def median(self, k=5): 
        self.img = cv2.medianBlur(self.img, k)
        return self
    def bilateral(self, d=9, s=75): 
        self.img = cv2.bilateralFilter(self.img, d, s, s)
        return self
    def clahe(self, c=3.0): 
        lab = cv2.cvtColor(self.img, cv2.COLOR_BGR2LAB)
        lab[:,:,0] = cv2.createCLAHE(clipLimit=c).apply(lab[:,:,0])
        self.img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
        return self
    def get(self): 
        return self.img
    def reset(self): 
        self.img = self.orig.copy()
        return self

# 使用示例:老旧人像修复(去噪 → 磨皮 → 提亮)
enhancer = EasyEnhancer("old_portrait.jpg")
final = enhancer.median(k=3).bilateral().clahe(c=2.5).get()
cv2.imwrite("old_portrait_enhanced.jpg", final)

6. Summary and filter selection guide

Problem scenarioPreferred methodSecond choice / matching solution
Uniform Gaussian Noise (Low Light/Film)Gaussian BlurNon-local Mean (High Precision Production Scenario)
Black and white salt and pepper noise (scratches/bit errors)Median filteringSmall kernel median + large kernel Gaussian combination (grind first and then remove small noise)
Portrait microdermabrasion / hard edge preservationBilateral filteringCLAHE + bilateral combination (first brightening and then microdermabrasion, less false colors)
Overall low contrast (shot on a cloudy day)Standard histogram equalizationCLAHE
Poor detail in local dark/highlight areas (backlit portrait)CLAHENone
1. For any enhancement and filtering operations, **first test on the grayscale image**, and then migrate to the color image after finding the appropriate parameters to avoid color distortion. 2. Try to increase the core size from small to large (3 → 5 → 7 → …), giving priority to ensuring operating efficiency (when processing videos or large images, try to control the core size within 15). 3. Don’t over-enhance or filter – over-smoothing can erase key details needed for edge detection and object recognition.