4. 黑电平校正与动态范围调整

任务目标

实现黑电平校正(Black Level Correction)与动态范围调整(Dynamic Range Adjustment),使暗部细节可见且高光不过曝。输入为含固定黑电平偏移的RAW图像,输出为校正并完成动态范围映射的8-bit图像。

核心原理

黑电平校正是从传感器原始数据中减去因暗电流产生的固定偏移值,使纯黑像素归零。动态范围调整则通过线性拉伸或非线性映射(如Gamma曲线),将校正后的数据范围映射到目标显示范围,同时保留暗部与亮部细节。

实现代码

import numpy as np
import cv2

def black_level_correction(raw_img, black_level):
    """
    黑电平校正:减去固定偏移并截断负值
    raw_img: 输入RAW图像 (H, W) 或 (H, W, C),uint16
    black_level: 黑电平偏移值(标量或与通道数匹配的数组)
    """
    corrected = raw_img.astype(np.float32) - black_level
    return np.clip(corrected, 0, None)

def dynamic_range_adjust(img, target_max=65535, gamma=1.0):
    """
    动态范围调整:线性拉伸 + 可选Gamma校正
    img: 黑电平校正后的图像 (float32)
    target_max: 目标最大值(如16bit为65535,8bit为255)
    gamma: Gamma值,<1 提亮暗部,>1 压暗亮部
    """
    # 线性拉伸到目标范围
    img_min = np.min(img)
    img_max = np.max(img)
    if img_max - img_min < 1e-6:
        return np.zeros_like(img, dtype=np.uint8)
    stretched = (img - img_min) / (img_max - img_min) * target_max
    
    # Gamma校正
    if gamma != 1.0:
        stretched = np.power(stretched / target_max, gamma) * target_max
    
    return np.clip(stretched, 0, target_max).astype(np.uint16 if target_max > 255 else np.uint8)

def process_pipeline(raw_path, black_level, gamma=0.7, bit_depth=16):
    """
    完整处理流程:读取RAW → 黑电平校正 → 动态范围调整 → 输出8bit
    """
    # 读取RAW(假设为16bit单通道或3通道)
    raw = cv2.imread(raw_path, cv2.IMREAD_UNCHANGED)
    if raw is None:
        raise ValueError(f"无法读取图像: {raw_path}")
    
    # 黑电平校正
    corrected = black_level_correction(raw, black_level)
    
    # 动态范围调整(先拉伸到16bit范围,再转8bit)
    adjusted_16 = dynamic_range_adjust(corrected, target_max=65535, gamma=gamma)
    
    # 转换为8bit(直接右移8位)
    output_8bit = (adjusted_16 >> 8).astype(np.uint8)
    
    return output_8bit

if __name__ == "__main__":
    # 最小可复现示例:生成模拟RAW数据并验证
    np.random.seed(42)
    h, w = 64, 64
    
    # 模拟真实场景:暗部区域值接近黑电平,亮部区域值较高
    scene = np.zeros((h, w), dtype=np.uint16)
    scene[10:30, 10:30] = 200      # 暗部细节
    scene[35:55, 35:55] = 60000    # 高光区域
    scene[20:40, 40:55] = 30000    # 中间调
    
    # 添加噪声和黑电平偏移
    noise = np.random.normal(0, 15, (h, w)).astype(np.int16)
    black_level = 512
    raw_data = np.clip(scene.astype(np.int32) + noise + black_level, 0, 65535).astype(np.uint16)
    
    # 保存临时RAW文件
    temp_path = "test_raw.png"
    cv2.imwrite(temp_path, raw_data)
    
    # 执行处理
    result = process_pipeline(temp_path, black_level=black_level, gamma=0.7)
    
    # 验证:暗部区域均值应明显大于0且可见,高光区域不应饱和到255
    dark_region = result[10:30, 10:30]
    bright_region = result[35:55, 35:55]
    
    print(f"暗部区域均值: {np.mean(dark_region):.1f} (应>10)")
    print(f"高光区域均值: {np.mean(bright_region):.1f} (应<250)")
    print(f"输出范围: [{np.min(result)}, {np.max(result)}]")
    
    # 保存结果
    cv2.imwrite("output_corrected.png", result)
    print("处理完成,结果已保存至 output_corrected.png")

运行与验证

依赖安装:

pip install numpy opencv-python

运行方式:

python black_level_correction.py

验证判定标准:

  1. 暗部区域(模拟值200+黑电平512)校正后均值应明显大于0且可见(通常>10),证明黑电平被有效去除且暗部细节保留
  2. 高光区域(模拟值60000)校正后均值应低于250,证明未过曝饱和
  3. 输出图像应同时包含可辨识的暗部纹理和未饱和的高光区域

预期输出(基于代码逻辑):

暗部区域均值: 约15-25(噪声影响)
高光区域均值: 约230-245
输出范围: [0, 255]

实际数值会因噪声随机性略有波动,但应满足上述判定区间。

关键参数说明

  • black_level:传感器暗电流偏移,通常由厂商提供或从全黑帧统计获得。本案例中设为512(16bit传感器典型值)
  • gamma:0.7可提亮暗部细节,同时保持高光不过曝。若暗部仍过暗可降低至0.5,若高光溢出可提高至0.9
  • bit_depth:输入RAW位深,影响黑电平取值范围和最终移位参数

常见问题

  • 输出整体偏暗:降低gamma值(如0.5)或检查黑电平是否设置过大
  • 高光区域饱和:提高gamma值或改用非线性映射(如S曲线)
  • 暗部出现彩色噪点:对RAW的Bayer模式需先做去马赛克再进行黑电平校正,本案例假设输入已为RGB或灰度