伽马变换:图像处理函数从理论到工程

伽马变换:图像处理函数从理论到工程

2025-03-28 图像处理课程笔记 代码链接:GitHub - yeisme/opencv_learn

理论回顾:不只是数学公式

伽马变换是图像处理中一种重要的非线性变换技术,主要用于调整图像的亮度和对比度

$$ s = c * r^{\gamma} $$
  • r 是输入像素值(通常归一化到 0-1 范围)
  • s 是输出像素值
  • c 是常数,通常设为 1
  • γ (gamma) 是伽马值,控制变换的曲线形状
    • γ < 1: 增强图像暗区细节,压缩亮区动态范围
    • γ > 1: 增强图像亮区细节,压缩暗区动态范围
    • γ = 1: 线性变换,输出等于输入

实践部分

原图

原图

使用 ffmpeg 修改为灰度图

1
ffmpeg -i img.jpg -vf format=gray -pix_fmt gray output_gray.jpg
灰度图

萌新版本:v1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 萌新写法:无任何注释,无任何异常处理,无任何参数检查,无任何性能优化,主打一个能跑就行
void gamma_v1(cv::Mat iMat, cv::Mat oMat, float gamma, float c)
{
    uchar input_data = 0;
    uchar output_data = 0;
    for (int i = 0; i < iMat.rows; i++)
    {
        for (int j = 0; j < iMat.cols; j++)
        {
            input_data = iMat.at<uchar>(i, j);
            output_data = c * std::pow(input_data, gamma);
            oMat.at<uchar>(i, j) = output_data;
        }
    }
}

调用

1
2
3
4
// gamma 变化后 0 < gamma  < 1
cv::Mat img_gamma_0_5(img.size(), img.type());
gamma_v1(img, img_gamma_0_5, 0.5, 1.0);
cv::imshow("Gamma 0.5", img_gamma_0_5);
出错图

很明显,这个萌新代码漏洞百出

问题等级问题描述潜在后果
致命未做数值归一化暗区细节完全丢失
致命直接对 uchar 做 pow 运算整数溢出导致图像噪点
严重无参数有效性检查gamma=0 等导致程序崩溃
严重Mat 对象值传递造成内存拷贝大图像处理内存暴涨
中度逐像素访问效率低下处理 2K 图像耗时超 500ms
轻微缺乏代码注释和异常处理可维护性差

这里有一点要说明

由于 OpenCV 的cv::Mat类设计的特性,尽管gamma_v1函数接收的是cv::Mat的值而不是引用,它仍然可以修改原始图像数据,原因如下:

  1. 智能指针结构cv::Mat是一个只包含头部信息和指向实际数据的指针的结构,它使用了引用计数机制。
  2. 仅复制头信息:当你通过值传递cv::Mat时,只有矩阵的头信息被复制,而实际的像素数据仍然共享同一块内存。
  3. 共享数据:函数内的oMat和函数外的img_gamma_v1指向相同的像素数据块,因此对oMat的修改直接影响原始数据。

第一次改进:v2 参数检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

/*
改进
1. 添加参数检查
2. 返回值改为 bool
3. oMat 不能溢出(像素值范围 0-255)
*/
bool gamma_v2(cv::Mat iMat, cv::Mat oMat, float gamma, float c, std::string &err)
{

    // 输入检查
    if (iMat.empty())
    {
        err = "Input image is empty";
        return false;
    }

    // 检查单通道
    if (iMat.channels() != 1)
    {
        err = "Input image is not single channel";
        return false;
    }

    // oMat 与 iMat 尺寸一致
    if (iMat.size() != oMat.size())
    {
        err = "Input image and output image size mismatch";
        return false;
    }

    // gamma 范围检查
    if (gamma <= 0)
    {
        err = "Gamma value must be greater than 0";
        return false;
    }

    // c 范围检查
    if (c <= 0)
    {
        err = "C value must be greater than 0";
        return false;
    }

    // gamma == 1 时,不需要处理
    if (gamma == 1)
    {
        iMat.copyTo(oMat);
        return true;
    }

    float input_data = 0;
    float output_data = 0;
    for (int i = 0; i < iMat.rows; i++)
    {
        for (int j = 0; j < iMat.cols; j++)
        {
            float result = c * std::pow(input_data, gamma);
            if (result > 255.0f)
            {
                output_data = 255;
            }
            else if (result < 0.0f)
            {
                output_data = 0;
            }
            else
            {
                output_data = static_cast<uchar>(result);
                oMat.at<uchar>(i, j) = output_data;
            }
        }
    }

    return true;
}

调用

1
2
3
4
5
6
7
auto img_gamma_v2 = cv::Mat(img.size(), img.type());
ok = gamma_v2(img, img_gamma_v2, 0.8, 1.0, err);
if (!ok)
{
    std::cerr << "Error: " << err << std::endl;
    return 1;
}

v1 和 v2 函数都调用 100 次,对比时间,发现基本没有影响,每次调用大概花费 120 ms

1
2
v1: 12732ms
v2: 4151ms

第二次改进:v3 多进程优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

/*
硬件优化
1. openmp 并行化
*/

#pragma omp parallel for
for (int i = 0; i < iMat.rows; i++)
{
    for (int j = 0; j < iMat.cols; j++)
    {
        // 设置为局部变量,避免多线程竞争
        float input_data = 0;
        float output_data = 0;
        input_data = iMat.at<uchar>(i, j);

        output_data = c * std::pow(input_data, gamma);
        if (output_data > 255)
        {
            output_data = 255;
        }
        else if (output_data < 0)
        {
            output_data = 0;
        }
        oMat.at<uchar>(i, j) = output_data;
    }
}

调用 100 次对比时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 优化版本 v3 openmp
auto start3 = utime::now();
for (int i = 0; i < 100; i++)
{
    auto img_gamma_v3 = cv::Mat(img.size(), img.type());
    ok = gamma_v3(img, img_gamma_v3, 0.8, 1.0, err);
    if (!ok)
    {
        std::cerr << "Error: " << err << std::endl;
        return 1;
    }
}
// cv::imwrite("Gamma_v3.jpg", img_gamma_v3);
auto end3 = utime::now();
std::cout << "v3: " << duration_cast<milliseconds>(end3 - start3).count() << "ms" << std::endl;
1
2
3
v1: 12732ms
v2: 4151ms
v3: 2206ms

每次调用花费约 23 ms,提升约 5 倍,但这个还不够,这只是一张 150k 左右的图片,还能怎么优化呢?

第三次改进:v4 算法优化 LUT

创建查找表,参数检查都不动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
算法优化
1. LUT 表优化
*/

// LUT 表
cv::Mat lookUpTable(1, 256, CV_8U);
uchar *lut = lookUpTable.ptr();
for (int i = 0; i < 256; i++)
{
    float result = c * std::pow(i, gamma);
    if (result > 255)
        lut[i] = 255;
    else if (result < 0)
        lut[i] = 0;
    else
        lut[i] = static_cast<uchar>(result);
}

#pragma omp parallel for
for (int i = 0; i < iMat.rows; i++)
{
    for (int j = 0; j < iMat.cols; j++)
    {
        uchar input_value = iMat.at<uchar>(i, j);
        oMat.at<uchar>(i, j) = lut[input_value];
    }
}
1
2
3
4
v1: 12732ms
v2: 4151ms
v3: 2206ms
v4: 1474ms

每次调用花费约 14 ms,从 120ms 到 14ms,性能提升了约 8.5 倍(缓存的魅力,空间换时间),将时间复杂度从 O(N^2)降为 O(N)

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus