2. 基于直方图的AE

任务说明

本案例实现一个基于直方图的自动曝光(AE)算法:输入一张图像及其当前曝光参数,算法分析图像亮度直方图分布,计算过曝/欠曝像素占比,据此调整曝光时间或增益,使直方图主体落在合理亮度区间(如16~235),避免过曝或欠曝。算法输出调整后的曝光参数,并给出调整前后直方图统计对比。

算法设计

核心思路:将图像转为灰度,统计256级直方图。定义暗部阈值(<16)与亮部阈值(>235),计算欠曝比(暗部像素占比)与过曝比(亮部像素占比)。若过曝比超过阈值(如5%),则降低曝光;若欠曝比超过阈值,则提高曝光。曝光调整量由直方图加权平均亮度与目标亮度(128)的偏差决定,采用比例控制并限制单次最大调整步长,防止振荡。

代码实现

import numpy as np
import cv2

def compute_histogram_stats(gray_img):
    """计算灰度直方图统计特征"""
    hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256]).flatten()
    total = gray_img.size
    # 欠曝:亮度 < 16;过曝:亮度 > 235
    under_exposed = hist[:16].sum() / total
    over_exposed = hist[236:].sum() / total
    # 加权平均亮度
    bins = np.arange(256)
    mean_brightness = (bins * hist).sum() / total
    return hist, under_exposed, over_exposed, mean_brightness

def histogram_ae_adjust(gray_img, current_exposure, target_mean=128.0,
                        under_thresh=0.05, over_thresh=0.05,
                        max_step=0.5, kp=0.8):
    """
    基于直方图的AE调整
    参数:
        gray_img: 灰度图像 (H,W) uint8
        current_exposure: 当前曝光参数(相对值,1.0为基准)
        target_mean: 目标平均亮度
        under_thresh: 欠曝像素占比阈值
        over_thresh: 过曝像素占比阈值
        max_step: 单次最大调整比例
        kp: 比例控制系数
    返回:
        new_exposure: 调整后的曝光参数
        stats: 调整前直方图统计字典
    """
    hist, under_exposed, over_exposed, mean_brightness = compute_histogram_stats(gray_img)
    stats = {
        'under_exposed_ratio': under_exposed,
        'over_exposed_ratio': over_exposed,
        'mean_brightness': mean_brightness,
        'histogram': hist
    }

    # 判断是否需要调整
    if over_exposed > over_thresh:
        # 过曝严重,降低曝光
        error = mean_brightness - target_mean
        delta = kp * (error / 255.0)
    elif under_exposed > under_thresh:
        # 欠曝严重,提高曝光
        error = target_mean - mean_brightness
        delta = kp * (error / 255.0)
    else:
        # 直方图分布合理,无需调整
        return current_exposure, stats

    # 限制单次调整步长
    delta = np.clip(delta, -max_step, max_step)
    new_exposure = current_exposure * (1.0 + delta)
    # 曝光参数保持合理范围
    new_exposure = np.clip(new_exposure, 0.1, 10.0)
    return new_exposure, stats

def apply_exposure_to_image(img, exposure):
    """模拟曝光调整效果(用于验证)"""
    # 线性增益模拟曝光变化
    adjusted = img.astype(np.float32) * exposure
    return np.clip(adjusted, 0, 255).astype(np.uint8)

# 示例:读取图像并执行AE
if __name__ == "__main__":
    # 生成一张模拟欠曝图像(均值约60)
    np.random.seed(42)
    synthetic_img = np.random.normal(60, 30, (480, 640)).astype(np.uint8)
    synthetic_img = np.clip(synthetic_img, 0, 255).astype(np.uint8)

    current_exp = 1.0
    new_exp, stats = histogram_ae_adjust(synthetic_img, current_exp)

    # 应用新曝光并重新统计
    adjusted_img = apply_exposure_to_image(synthetic_img, new_exp)
    _, new_under, new_over, new_mean = compute_histogram_stats(adjusted_img)

    print(f"当前曝光: {current_exp:.2f}")
    print(f"调整后曝光: {new_exp:.2f}")
    print(f"调整前: 均值={stats['mean_brightness']:.1f}, "
          f"欠曝比={stats['under_exposed_ratio']*100:.1f}%, "
          f"过曝比={stats['over_exposed_ratio']*100:.1f}%")
    print(f"调整后: 均值={new_mean:.1f}, "
          f"欠曝比={new_under*100:.1f}%, "
          f"过曝比={new_over*100:.1f}%")

关键逻辑说明

算法首先统计直方图并计算欠曝/过曝占比。当欠曝比超过5%时判定为欠曝,提高曝光;当过曝比超过5%时判定为过曝,降低曝光。调整量由当前平均亮度与目标亮度(128)的偏差经比例控制计算,并限制单次调整不超过50%,保证收敛稳定性。示例中生成一张均值约60的模拟欠曝图像,算法应输出大于1.0的新曝光参数,使调整后图像均值接近128。

验证方法

运行上述代码,观察输出:

  • 调整前欠曝比应显著高于5%,均值明显低于128;
  • 调整后曝光参数应大于1.0(提高曝光);
  • 调整后均值应比调整前更接近128,欠曝比应下降至5%以下或明显减小。

判定标准:调整后图像平均亮度落在100~156区间(目标128±28),且欠曝比低于调整前数值,即认为算法有效完成曝光补偿。

基于直方图的AE算法流程 输入图像 (灰度/RAW) 直方图统计 256级灰度分布 欠曝/过曝占比 过曝>5%? 或欠曝>5%? 是 曝光参数调整 比例控制+步长限制 否 保持当前曝光参数 输出新曝光参数 + 直方图统计对比 调整前直方图 调整后直方图 暗部(<16) 亮部(>235) 暗部(<16) 亮部(>235)

预期输出

对于均值约60的模拟欠曝图像,算法判定欠曝比超过5%,输出曝光参数约为1.5~2.0(具体值取决于随机分布),调整后图像均值应提升至100以上,欠曝比显著下降。代码中打印的调整前后统计值即为验证依据。