深入探究图像增强(C语言实现)

我们将从基础出发使用C语言进行图像处理与分析,重点讨论图像增强和平滑技术。图像增强技术旨在通过增加对比度、亮度和整体清晰度来改善图像的视觉质量。另一方面,图像平滑方法则用于减少噪声并减少图像中的突变,使图像更加均匀和视觉上吸引人。

使用C语言作为实现方式是因为只有当你手敲一遍这些代码,你才会对这些方法有更加深刻的理解


我们在前两次的笔记中已经实现了图像处理的基本操作

分别是

  • 图像每一个像素灰度值的读入
  • 图像灰度值直方图的显示

今天的目标实际上也很小,我们的目标就是实现图像增强

书上一共讲了两种办法,分别是

  1. 灰度线性拉伸算法
  2. 直方图均衡化

我们一个一个来尝试

灰度线性拉伸算法

我们先来看代码

void LinearStretchDemo(BYTE* pGryImg, int width, int height, double k, double b)
{
    BYTE* pCur, * pEnd;
    for(pCur = pGryImg,pEnd = pGryImg + width + height;pCur < pEnd;)
        //这个地方书上是这么写的,我在下面还是写成了标准写法
        {
            *(pCur++) = LUT[ * pCur];
        }
    return;
}

总体上来看还是比较简单,我们来试着写成C语言版本

void LinearStretchDemo(uint8_t* pGryImg, int width, int height, double k, double b) 
{
    uint8_t* pCur, * pEnd;
    for (pCur = pGryImg, pEnd = pGryImg + width * height; pCur < pEnd; pCur++) 
    {
        *pCur = LUT[*pCur];
    }
}

这个代码前面讲过好多了,这里就不再多讲了,有兴趣可以去看看C语言图像的读入那一篇笔记

但是这里面LUT是什么意思呢?

LUT 是 Look-Up Table 的缩写,中文意思是查找表或者映射表。在图像处理中,LUT 是一种非常常见的技机,用于对图像进行颜色或灰度值的映射和调整。

具体来说,LUT 是一个数组或者表格,其中存储了输入值到输出值的对应关系。在图像处理中,通常用 LUT 来实现颜色校正、对比度调整、灰度拉伸等操作。例如,在灰度拉伸中,LUT 存储了原始灰度值到拉伸后的灰度值之间的映射关系。

使用 LUT 的好处在于,它可以提高图像处理的效率,并且允许我们通过简单的表格查询来实现复杂的颜色或灰度值调整。同时,LUT 也可以在不同的图像处理算法中重复使用,提高了算法的复用性和可维护性。

总而言之,LUT 是图像处理中非常有用的工具,它通过预先计算和存储输入值到输出值的映射关系,帮助我们快速、高效地对图像进行颜色和灰度值的调整。

这里如果每个灰度值都有一个映射值,那么就不用进行重复大量的计算了,只需要计算255次即可

这么说不是很好理解,我们直接来看书上下一个算法

这里我直接写成C语言版本

void LinearStretchDemo(uint8_t* pGryImg, int width, int height, double k, double b)
{
    uint8_t* pCur, * pEnd;
    int LUT[256];    //因为只有[0,255]共256个灰度值
    
    //step1. 生成查找表
    for (int g = 0; g < 256; g++)
    {
        LUT[g] = max(0, min(255, k * g + b));
    }
    
    //step2. 进行变换
    for (pCur = pGryImg, pEnd = pGryImg + width * height; pCur < pEnd; pCur++)
        {
            *pCur = LUT[*pCur];
        }
    //step3. 结束
    return;
}

这段代码是一个实现灰度图像线性拉伸处理的函数 LinearStretchDemo。让我来逐步解释这段代码的具体实现:

  1. uint8_t* pCur, * pEnd;:定义了两个指针变量,pCur 用于指向当前处理的像素值,pEnd 指向图像数据的末尾。
  2. int LUT[256];:定义了一个大小为 256 的整型数组,用于存储灰度值的映射关系。
  3. 生成查找表部分:
    • for (int g = 0; g < 256; g++):遍历所有可能的灰度值(0 到 255)。
    • LUT[g] = max(0, min(255, k * g + b));:对于每个灰度值,根据线性拉伸的公式 k * g + b 计算新的灰度值,并确保其范围在 0 到 255 之间,以防止越界。
  4. 进行变换部分:
    • for (pCur = pGryImg, pEnd = pGryImg + width * height; pCur < pEnd; pCur++):遍历图像数据中的每个像素。
    • *pCur = LUT[*pCur];:使用查找表 LUT 将当前像素的灰度值映射为线性拉伸后的新灰度值。
  5. 返回处理结果并结束函数。
我们来试一下

首先来讲一下传参

image-20240419193010726

  1. pGryImg:这是一个指向灰度图像数据的指针。灰度图像是一个二维数组,存储了图像中每个像素的灰度值。通过这个指针,函数能够访问图像的像素数据。
  2. width:这是图像的宽度,表示图像中每行像素的数量。它告诉函数每行有多少像素数据。
  3. height:这是图像的高度,表示图像中有多少行像素。它告诉函数图像有多少行数据。
  4. k:这是一个 double 类型的参数,代表线性拉伸的斜率。它控制着拉伸的速率或程度。当 ( k ) 大于 1 时,图像的对比度增加;当 ( k ) 小于 1 时,对比度降低。
  5. b:这也是一个 double 类型的参数,代表线性拉伸的偏移。它控制着拉伸后灰度值的起始位置。当 ( b ) 大于 0 时,图像的整体亮度增加;当 ( b ) 小于 0 时,整体亮度减小。

看效果

image-20240419194326354

简单的处理以后效果其实还是不错的

我们用这两个函数看一下数值

这两个函数的讲解在这篇文章

图像处理与图像分析—图像统计特性的计算(纯C语言实现灰度值显示)-CSDN博客

//统计图像灰度值
//pImg:灰度图像数据的指针。
//width:图像的宽度。
//height:图像的高度。
//* histogram:数组首元素地址,需要一个能储存256个变量的整型数组
void GetHistogram(uint8_t* pImg, int width, int height, int* histogram)
{
    uint8_t* pCur;
    uint8_t* pEnd = pImg + width * height;

    // 初始化直方图数组
    memset(histogram, 0, sizeof(int) * 256);

    // 直方图统计
    for (pCur = pImg; pCur < pEnd;)
    {
        histogram[*pCur]++;
        pCur++;
    }

    // 函数结束
    return;
}

//亮度和对比度
//储存histogram灰度直方图的指针
//接收亮度的变量地址
//接收对比度的变量地址
void GetBrightContrast(int* histogram, double* bright, double* contrast)
{
    int g;
    double sum, num; //书上说图像很亮时,int有可能会溢出,所以我这里直接用double
    double fsum;

    //step.1 求亮度
    for (sum = num = 0, g = 0; g < 256; g++)
    {
        sum += histogram[g] * g;
        num += histogram[g];
    }
    *bright = sum * 1.0 / num;

    //step.2 求对比度
    for (fsum = 0.0, g = 0; g < 256; g++)
    {
        fsum += histogram[g] * (g - *bright) * (g - *bright);
    }
    *contrast = sqrt(fsum / (num - 1)); //即Std Dev

    //step.3 结束
    return;
}

image-20240419202007702

直方图的均衡化与规定化

image-20240419202356451

其实很好理解,就是把集中在某一区域的灰度值均匀的平铺在整体区域

我们还是先来看书上的代码

void RmwHistogramEqualizeDemo(BYTE *pGryImg, int width, int height)
{
    // 定义变量
    BYTE *pCur, *pEnd = pGryImg + width * height; // 指针变量,指向当前像素和图像末尾
    int histogram[256], A[256], LUT[256], g; // 直方图数组、累积直方图数组、查找表和灰度级

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;)
        histogram[*(pCur++)]++; // 统计每个灰度级出现的频率

    // step.2-------------求A[g],N-------------------------//
    for (g = 1, A[0] = histogram[0]; g < 256; g++)
    {
        A[g] = A[g - 1] + histogram[g]; // 计算累积直方图数组
    }

    // step.3-------------求LUT[g]-------------------------//
    for (g = 0; g < 256; g++)
        LUT[g] = 255 * A[g] / (width * height); // 计算直方图均衡化后的灰度级

    // step.4-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;)
        *(pCur++) = LUT[*pCur]; // 使用查找表对每个像素进行映射

    // step.5-------------结束------------------------------//
    return;
}

是不是还能继续优化

void RmwHistogramEqualize(BYTE *pGryImg, int width, int height)
{
    BYTE *pCur, *pEnd = pGryImg + width * height; // 指针变量,指向当前像素和图像末尾
    int histogram[256], LUT[256], A, g; // 直方图数组、查找表数组、累积直方图、灰度级

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;) 
        histogram[*(pCur++)]++; // 统计每个灰度级出现的频率

    // step.2-------------求LUT[g]-------------------------//
    A = histogram[0]; // 初始化累积直方图的值为第一个灰度级的频率
    LUT[0] = 255 * A / (width * height); // 计算第一个灰度级对应的均衡化后的灰度值
    for (g = 1; g < 256; g++)
    {
        A += histogram[g]; // 更新累积直方图的值
        LUT[g] = 255 * A / (width * height); // 计算当前灰度级对应的均衡化后的灰度值
    }

    // step.3-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;) 
        *(pCur++) = LUT[*pCur]; // 使用查找表对每个像素进行灰度映射

    // step.4-------------结束------------------------------//
    return;
}

接下来改为C语言版

void RmwHistogramEqualize(uint8_t *pGryImg, int width, int height) 
{
    uint8_t *pCur, *pEnd = pGryImg + width * height; // 指针变量,指向当前像素和图像末尾
    int histogram[256], LUT[256], A, g; // 直方图数组、查找表数组、累积直方图、灰度级

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;)  
        histogram[*(pCur++)]++; // 统计每个灰度级出现的频率

    // step.2-------------求LUT[g]-------------------------//
    A = histogram[0]; // 初始化累积直方图的值为第一个灰度级的频率
    LUT[0] = 255 * A / (width * height); // 计算第一个灰度级对应的均衡化后的灰度值
    for (g = 1; g < 256; g++)  {
        A += histogram[g]; // 更新累积直方图的值
        LUT[g] = 255 * A / (width * height); // 计算当前灰度级对应的均衡化后的灰度值
    }

    // step.3-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;)  
        *(pCur++) = LUT[*pCur]; // 使用查找表对每个像素进行灰度映射

    // step.4-------------结束------------------------------//
    return;
}

我们来看看实现效果怎么样

image-20240419215512601

image-20240419222847826

可以看到效果还是很不错的

对数变换

人眼对于亮度变化的反应是随着光的增加而减弱,实验证明人眼的这种性质更近似于对数函数

对数变换是一种常见的图像处理技术,通常用于增强图像的对比度或调整图像的亮度。它的原理是通过对图像的像素值取对数来调整像素值的分布,从而改变图像的外观。

在对数变换中,常用的是自然对数函数(以e为底的对数函数),其公式为:

image-20240419221718879

其中:

  • ( s ) 是输出图像的像素值;
  • ( r ) 是输入图像的像素值;
  • ( c ) 是一个常数,用于调节对比度;
  • ( \log ) 是自然对数函数。

对数变换的特点包括:

  1. 对数压缩特性:对于输入像素值较小的区域,对数变换会对其进行较大程度的拉伸,从而增强了图像的对比度。这对于那些像素值分布在较低灰度级区域的图像非常有用,可以使得细节更加清晰可见。
  2. 对数拉伸特性:对于输入像素值较大的区域,对数变换会对其进行较小程度的拉伸,这有助于将高灰度级的区域拉伸到更广泛的灰度范围内,从而增强了图像的亮度表现。

对数变换的应用包括但不限于:

  • 图像增强:通过调整对数变换中的参数,可以增强图像的对比度,使得图像细节更加清晰。
  • 图像压缩:对数变换也可以用于压缩图像的动态范围,将大范围的灰度级映射到一个较小的范围内,从而方便存储和传输。

看代码

void RmwLogTransform(BYTE *pGryImg, int width, int height)
{
    BYTE *pCur, *pEnd = pGryImg + width * height; // 指向灰度图像数据的当前指针和结束指针
    int histogram[256], LUT[256], gmax, g; // 声明直方图数组、查找表数组、最大灰度值、当前灰度值
    double c; // 声明常数c

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;) 
        histogram[*(pCur++)]++; // 遍历图像数据,统计每个灰度级的像素数量

    // step.2-------------最大值---------------------------//
    for (gmax = 255; gmax >= 0; gmax++) 
        if (histogram[gmax]) break; // 从最大灰度级开始向低灰度级搜索,找到第一个非零灰度级,即最大灰度值

    // step.3-------------求LUT[g]-------------------------//
    c = 255.0 / log(1 + gmax); // 计算常数c
    for (g = 0; g < 256; g++) 
    {
        LUT[g] = (int)(c * log(1 + g)); // 根据对数变换公式计算查找表中每个灰度级的映射值
    }

    // step.4-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;) 
        *(pCur++) = LUT[*pCur]; // 使用查找表将图像数据进行对数变换

    // step.5-------------结束------------------------------//
    return; // 函数结束
}

改为C语言实现

void RmwLogTransform(uint8_t *pGryImg, int width, int height)
{
    uint8_t *pCur, *pEnd = pGryImg + width * height; // 指向灰度图像数据的当前指针和结束指针
    int histogram[256], LUT[256], gmax, g; // 声明直方图数组、查找表数组、最大灰度值、当前灰度值
    double c; // 声明常数c

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;) 
        histogram[*(pCur++)]++; // 遍历图像数据,统计每个灰度级的像素数量

    // step.2-------------最大值---------------------------//
    for (gmax = 255; gmax >= 0; gmax++) 
        if (histogram[gmax]) break; // 从最大灰度级开始向低灰度级搜索,找到第一个非零灰度级,即最大灰度值

    // step.3-------------求LUT[g]-------------------------//
    c = 255.0 / log(1 + gmax); // 计算常数c
    for (g = 0; g < 256; g++) 
    {
        LUT[g] = (int)(c * log(1 + g)); // 根据对数变换公式计算查找表中每个灰度级的映射值
    }

    // step.4-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;) 
        *(pCur++) = LUT[*pCur]; // 使用查找表将图像数据进行对数变换

    // step.5-------------结束------------------------------//
    return; // 函数结束
}

image-20240419222755214

增强方法更应该根据我们的需求来选择


源码

IDP.h

#pragma once

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
 
uint8_t* readGrayScaleBMP(const char* filename, int* width, int* height);//读取8位灰度图片
void saveGrayScaleBMP(const char* filename, const uint8_t* imageData, int width, int height);// 将8位灰度图像数据保存为BMP文件
uint8_t* readColorBMP(const char* filename, int* width, int* height);//读取24位彩色图像的BMP文件
void saveColorBMP(const char* filename, const uint8_t* imageData, int width, int height);//将24位彩色图像数据保存为BMP文件
void LinearStretchDemo(uint8_t* pGryImg, int width, int height, double k, double b);//灰度线性拉伸
void GetHistogram(uint8_t* pImg, int width, int height, int* histogram);//统计图像灰度值
void GetBrightContrast(int* histogram, double* bright, double* contrast);//亮度和对比度
void RmwHistogramEqualize(uint8_t* pGryImg, int width, int height);//直方图均衡化
void RmwLogTransform(uint8_t* pGryImg, int width, int height);//对数变换

IDP.C

#include "IDP.h"

//读取8位灰度图片
//filename:字符数组的指针,用于指定要保存的图像文件的名称或路径。
//imageData:无符号 8 位整型数据的指针,代表要保存的图像数据。
//width:图像的宽度。
//height:图像的高度。
uint8_t* readGrayScaleBMP(const char* filename, int* width, int* height) 
{
    FILE* file = fopen(filename, "rb");
    if (!file) {
        fprintf(stderr, "Error opening file %s\n", filename);
        return NULL;
    }

    // 读取BMP文件头部信息
    uint8_t bmpHeader[54];
    fread(bmpHeader, 1, 54, file);

    // 从文件头部提取图像宽度和高度信息
    *width = *(int*)&bmpHeader[18];
    *height = *(int*)&bmpHeader[22];

    // 分配存储图像数据的内存
    uint8_t* imageData = (uint8_t*)malloc(*width * *height);
    if (!imageData) {
        fprintf(stderr, "内存分配失败\n");
        fclose(file);
        return NULL;
    }

    // 计算调色板的大小
    int paletteSize = *(int*)&bmpHeader[46];
    if (paletteSize == 0)
        paletteSize = 256;

    // 读取调色板数据
    uint8_t palette[1024];
    fread(palette, 1, paletteSize * 4, file);

    // 读取图像数据
    fseek(file, *(int*)&bmpHeader[10], SEEK_SET);
    fread(imageData, 1, *width * *height, file);

    fclose(file);

    return imageData;
}

// 将8位灰度图像数据保存为BMP文件
//filename:字符数组的指针,用于指定要保存的图像文件的名称或路径。
//imageData:无符号 8 位整型数据的指针,代表要保存的图像数据。
//width:图像的宽度。
//height:图像的高度。
void saveGrayScaleBMP(const char* filename, const uint8_t* imageData, int width, int height) 
{
    FILE* file = fopen(filename, "wb");
    if (!file) {
        fprintf(stderr, "Error creating file %s\n", filename);
        return;
    }

    // BMP文件头部信息
    uint8_t bmpHeader[54] = {
        0x42, 0x4D,             // 文件类型标识 "BM"
        0x36, 0x00, 0x0C, 0x00, // 文件大小(以字节为单位,此处假设图像数据大小不超过4GB)
        0x00, 0x00,             // 保留字段
        0x00, 0x00,             // 保留字段
        0x36, 0x00, 0x00, 0x00, // 位图数据偏移(以字节为单位)
        0x28, 0x00, 0x00, 0x00, // 位图信息头大小(40字节)
        0x00, 0x00, 0x00, 0x00, // 图像宽度
        0x00, 0x00, 0x00, 0x00, // 图像高度
        0x01, 0x00,             // 目标设备的级别(此处为1,不压缩)
        0x08, 0x00,             // 每个像素的位数(8位)
        0x00, 0x00, 0x00, 0x00, // 压缩类型(此处为不压缩)
        0x00, 0x00, 0x00, 0x00, // 图像数据大小(以字节为单位,此处为0,表示不压缩)
        0x00, 0x00, 0x00, 0x00, // 水平分辨率(像素/米,此处为0,表示未知)
        0x00, 0x00, 0x00, 0x00, // 垂直分辨率(像素/米,此处为0,表示未知)
        0x00, 0x00, 0x00, 0x00, // 使用的颜色索引数(0表示使用所有调色板项)
        0x00, 0x00, 0x00, 0x00  // 重要的颜色索引数(0表示所有颜色都重要)
    };

    // 更新BMP文件头部信息中的宽度和高度
    *(int*)&bmpHeader[18] = width;
    *(int*)&bmpHeader[22] = height;

    // 写入BMP文件头部信息
    fwrite(bmpHeader, 1, 54, file);

    // 写入调色板数据
    for (int i = 0; i < 256; i++) {
        fputc(i, file);  // 蓝色分量
        fputc(i, file);  // 绿色分量
        fputc(i, file);  // 红色分量
        fputc(0, file);  // 保留字节
    }

    // 写入图像数据
    fwrite(imageData, 1, width * height, file);

    fclose(file);
}

// 读取24位彩色图像的BMP文件
//filename:字符数组的指针,用于指定要读取的 BMP 格式图像文件的名称或路径。
//width:整型变量的指针,用于存储读取的图像的宽度。
//height:整型变量的指针,用于存储读取的图像的高度。
uint8_t* readColorBMP(const char* filename, int* width, int* height) 
{
    FILE* file = fopen(filename, "rb");
    if (!file) {
        fprintf(stderr, "Error opening file %s\n", filename);
        return NULL;
    }

    // 读取BMP文件头部信息
    uint8_t bmpHeader[54];
    fread(bmpHeader, 1, 54, file);

    // 从文件头部提取图像宽度和高度信息
    *width = *(int*)&bmpHeader[18];
    *height = *(int*)&bmpHeader[22];

    // 分配存储图像数据的内存
    uint8_t* imageData = (uint8_t*)malloc(*width * *height * 3);
    if (!imageData) {
        fprintf(stderr, "Memory allocation failed\n");
        fclose(file);
        return NULL;
    }

    // 读取图像数据
    fseek(file, *(int*)&bmpHeader[10], SEEK_SET);
    fread(imageData, 1, *width * *height * 3, file);

    fclose(file);

    return imageData;
}

//将24位彩色图像数据保存为BMP文件
//filename:字符数组的指针,用于指定要保存的图像文件的名称或路径。
//imageData:无符号 8 位整型数据的指针,代表要保存的图像数据。
//width:图像的宽度。
//height:图像的高度。
void saveColorBMP(const char* filename, const uint8_t* imageData, int width, int height) 
{
    FILE* file = fopen(filename, "wb");
    if (!file) {
        fprintf(stderr, "Error creating file %s\n", filename);
        return;
    }

    // BMP文件头部信息
    uint8_t bmpHeader[54] = {
        0x42, 0x4D,             // 文件类型标识 "BM"
        0x00, 0x00, 0x00, 0x00, // 文件大小(占位,稍后计算)
        0x00, 0x00,             // 保留字段
        0x00, 0x00,             // 保留字段
        0x36, 0x00, 0x00, 0x00, // 位图数据偏移(以字节为单位)
        0x28, 0x00, 0x00, 0x00, // 位图信息头大小(40字节)
        0x00, 0x00, 0x00, 0x00, // 图像宽度
        0x00, 0x00, 0x00, 0x00, // 图像高度
        0x01, 0x00,             // 目标设备的级别(此处为1,不压缩)
        0x18, 0x00,             // 每个像素的位数(24位)
        0x00, 0x00, 0x00, 0x00, // 压缩类型(此处为不压缩)
        0x00, 0x00, 0x00, 0x00, // 图像数据大小(占位,稍后计算)
        0x00, 0x00, 0x00, 0x00, // 水平分辨率(像素/米,此处为0,表示未知)
        0x00, 0x00, 0x00, 0x00, // 垂直分辨率(像素/米,此处为0,表示未知)
        0x00, 0x00, 0x00, 0x00, // 使用的颜色索引数(0表示使用所有调色板项)
        0x00, 0x00, 0x00, 0x00  // 重要的颜色索引数(0表示所有颜色都重要)
    };

    // 更新BMP文件头部信息中的宽度和高度
    *(int*)&bmpHeader[18] = width;
    *(int*)&bmpHeader[22] = height;

    // 计算图像数据大小
    uint32_t imageDataSize = width * height * 3 + 54; // 加上文件头部大小
    bmpHeader[2] = (uint8_t)(imageDataSize & 0xFF);
    bmpHeader[3] = (uint8_t)((imageDataSize >> 8) & 0xFF);
    bmpHeader[4] = (uint8_t)((imageDataSize >> 16) & 0xFF);
    bmpHeader[5] = (uint8_t)((imageDataSize >> 24) & 0xFF);

    // 写入BMP文件头部信息
    fwrite(bmpHeader, 1, 54, file);

    // 写入图像数据
    fwrite(imageData, width * height * 3, 1, file);

    fclose(file);
}

//灰度线性拉伸
//pGryImg:灰度图像数据的指针。
//width:图像的宽度。
//height:图像的高度。
//k:线性拉伸的斜率。它控制着拉伸的速率或程度。当(k) 大于 1 时,图像的对比度增加;当(k) 小于 1 时,对比度降低。
//b:线性拉伸的偏移。它控制着拉伸后灰度值的起始位置。当(b) 大于 0 时,图像的整体亮度增加;当(b) 小于 0 时,整体亮度减小。
void LinearStretchDemo(uint8_t* pGryImg, int width, int height, double k, double b)
{
    uint8_t* pCur, * pEnd;
    int LUT[256];    //因为只有[0,255]共256个灰度值

    //step1. 生成查找表
    for (int g = 0; g < 256; g++)
    {
        LUT[g] = max(0, min(255, k * g + b));
    }

    //step2. 进行变换
    for (pCur = pGryImg, pEnd = pGryImg + width * height; pCur < pEnd; pCur++)
    {
        *pCur = LUT[*pCur];
    }
    //step3. 结束
    return;
}

//统计图像灰度值
//pImg:灰度图像数据的指针。
//width:图像的宽度。
//height:图像的高度。
//* histogram:数组首元素地址,需要一个能储存256个变量的整型数组
void GetHistogram(uint8_t* pImg, int width, int height, int* histogram)
{
    uint8_t* pCur;
    uint8_t* pEnd = pImg + width * height;

    // 初始化直方图数组
    memset(histogram, 0, sizeof(int) * 256);

    // 直方图统计
    for (pCur = pImg; pCur < pEnd;)
    {
        histogram[*pCur]++;
        pCur++;
    }

    // 函数结束
    return;
}

//亮度和对比度
//储存histogram灰度直方图的指针
//接收亮度的变量地址
//接收对比度的变量地址
void GetBrightContrast(int* histogram, double* bright, double* contrast)
{
    int g;
    double sum, num; //书上说图像很亮时,int有可能会溢出,所以我这里直接用double
    double fsum;

    //step.1 求亮度
    for (sum = num = 0, g = 0; g < 256; g++)
    {
        sum += histogram[g] * g;
        num += histogram[g];
    }
    *bright = sum * 1.0 / num;

    //step.2 求对比度
    for (fsum = 0.0, g = 0; g < 256; g++)
    {
        fsum += histogram[g] * (g - *bright) * (g - *bright);
    }
    *contrast = sqrt(fsum / (num - 1)); //即Std Dev

    //step.3 结束
    return;
}

//pGryImg:灰度图像数据的指针。
//width:图像的宽度。
//height:图像的高度。
void RmwHistogramEqualize(uint8_t* pGryImg, int width, int height)
{
    uint8_t* pCur, * pEnd = pGryImg + width * height; // 指针变量,指向当前像素和图像末尾
    int histogram[256], LUT[256], A, g; // 直方图数组、查找表数组、累积直方图、灰度级

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;)
        histogram[*(pCur++)]++; // 统计每个灰度级出现的频率

    // step.2-------------求LUT[g]-------------------------//
    A = histogram[0]; // 初始化累积直方图的值为第一个灰度级的频率
    LUT[0] = 255 * A / (width * height); // 计算第一个灰度级对应的均衡化后的灰度值
    for (g = 1; g < 256; g++) {
        A += histogram[g]; // 更新累积直方图的值
        LUT[g] = 255 * A / (width * height); // 计算当前灰度级对应的均衡化后的灰度值
    }

    // step.3-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;)
        *(pCur++) = LUT[*pCur]; // 使用查找表对每个像素进行灰度映射

    // step.4-------------结束------------------------------//
    return;
}

//对数变换
//pGryImg:灰度图像数据的指针。
//width:图像的宽度。
//height:图像的高度。
void RmwLogTransform(uint8_t* pGryImg, int width, int height)
{
    uint8_t* pCur, * pEnd = pGryImg + width * height; // 指向灰度图像数据的当前指针和结束指针
    int histogram[256], LUT[256], gmax, g; // 声明直方图数组、查找表数组、最大灰度值、当前灰度值
    double c; // 声明常数c

    // step.1-------------求直方图--------------------------//
    memset(histogram, 0, sizeof(int) * 256); // 初始化直方图数组为0
    for (pCur = pGryImg; pCur < pEnd;)
        histogram[*(pCur++)]++; // 遍历图像数据,统计每个灰度级的像素数量

    // step.2-------------最大值---------------------------//
    for (gmax = 255; gmax >= 0; gmax++)
        if (histogram[gmax]) break; // 从最大灰度级开始向低灰度级搜索,找到第一个非零灰度级,即最大灰度值

    // step.3-------------求LUT[g]-------------------------//
    c = 255.0 / log(1 + gmax); // 计算常数c
    for (g = 0; g < 256; g++)
    {
        LUT[g] = (int)(c * log(1 + g)); // 根据对数变换公式计算查找表中每个灰度级的映射值
    }

    // step.4-------------查表------------------------------//
    for (pCur = pGryImg; pCur < pEnd;)
        *(pCur++) = LUT[*pCur]; // 使用查找表将图像数据进行对数变换

    // step.5-------------结束------------------------------//
    return; // 函数结束
}

次回预告

图像的质量是什么,如何提高图像的质量,中值滤波,均值滤波,最小值滤波,最大值滤波,高斯滤波,二值图像滤波,数学形态滤波,条件滤波又都代表着什么?

关于噪声的处理方法,下一篇博文将会讲解图像平滑

感谢您的阅读~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/559143.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

利用二维码定位技术实现桌面机器人简易定位方案(上篇)

目录 1、前言2、二维码的定位标签识别原理3、生成定位标签3、基于定位标签的物体识别与定位 1、前言 机械手臂尤其是工业场景下大部分的应用是在一个平面&#xff08;桌面&#xff09;内完成一些抓取工作。一般可以用示教方式完成重复步骤。但是示教方式&#xff0c;对于一些活…

过氧化氢滴定方法可用的PFA器皿有哪些?

滴定液:KMnO4标准溶液 试液:H2O2商品液(3%)&#xff0c;H2SO4 (3.0mol/L ) 指示剂:酚酞指示剂 仪器:分析天平&#xff0c;PFA酸式滴定管50mL&#xff0c;PFA 移液管10mL/25mL、PFA 容量瓶250mL、PFA锥形瓶250mL 1、KMnO4标准溶液浓度的标定(见实验:高锰酸钾标准溶液的配制与…

记一次普通的单表查询sql优化,去掉文件排序

一现象&#xff1a; 有空观察了线上某个sql语句执行计划&#xff0c;发现在500多毫秒左右&#xff0c;打算进行下优化。 二步骤&#xff1a; 对查询列assessment_periodic_id、assessment_user_id、create_time添加了组合索引并指定了倒叙。加入create_time 使查询结果不需要在…

阿里云OSS 存储对象的注册与使用

目录 一、什么是阿里云OSS 二、 点击免费试用 2.1 选择第一个&#xff0c;点击免费试用 ​编辑 2.2 登录管理控制台 2.3 进入Bucket 2.4、在阿里云网站上的个人中心配置Accesskey,查询accessKeyId和accessKeySecret。 2.5、进入AccssKey管理页面应该会出现下图提示&…

通用大模型研究重点之五:llama family

LLAMA Family decoder-only类型 LLaMA&#xff08;Large Language Model AI&#xff09;在4月18日公布旗下最大模型LLAMA3&#xff0c;参数高达4000亿。目前meta已经开源了80亿和700亿版本模型&#xff0c;主要升级是多模态、长文本方面工作。 模型特点&#xff1a;采用标准的…

Java面试八股之Java异常处理完成后,Exception对象会发生什么变化

Java异常处理完成后&#xff0c;Exception对象会发生什么变化 这个题的难度在于&#xff0c;看到题之后可能不知道面试官想问什么。在面试中&#xff0c;如果实在没明白&#xff0c;可以让面试官再深入阐述一下。 Java异常处理完成后&#xff0c;Exception对象失去了程序中的…

自定义Blazor单文件Web程序端口

#接 上篇 Mysql快速迁移版的制作过程# 上一篇《Mysql8快速迁移版的制作过程》完成了快速迁移的数据库的准备&#xff0c;今天接着讲基于Blazor的Web程序快速迁移版的制作。 单文件发布的难点不在发布而是因为程序系统默认给了个5001的端口&#xff0c;而是如何能够让用户自定…

Leetcode 11.盛最多水的容器(暴力->双指针)

给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容器。 示例 …

Postman调用OpenApi接口

首先你需要科学上网。。。。。 请求方式&#xff1a;post 请求地址&#xff1a;https://api.openai.com/v1/chat/completions 请求头&#xff1a; Authorization : Bearer key Content-Type : application/json Body : { "messages": [{ "role": &quo…

【精简改造版】大型多人在线游戏BrowserQuest服务器Golang框架解析(1)——功能清单

1.匿名登录 2.服务连接 3.新手引导 4.随机出生点 5.界面布局 6.玩法帮助 7.NPC会话 8.成就系统 9.成就达成 10.用户聊天 11.战斗&信息展示 12.药水使用 13.副本传送 14.玩家死亡 15.超时断开

OpenHarmony 视图缩放组件—subsampling-scale-image-view

简介 深度缩放视图&#xff0c;图像显示&#xff0c;手势平移缩放双击等 效果图&#xff08;旋转、缩放、平移&#xff09; 下载安装 ohpm install ohos/subsampling-scale-image-view OpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何安装 OpenHarmony ohpm 包 使…

Servlet第四篇【request对象常用方法、应用】

什么是HttpServletRequest HttpServletRequest对象代表客户端的请求&#xff0c;当客户端通过HTTP协议访问服务器时&#xff0c;HTTP请求头中的所有信息都封装在这个对象中&#xff0c;开发人员通过这个对象的方法&#xff0c;可以获得客户这些信息。 简单来说&#xff0c;要得…

mysql四种引擎区别

MySQL 提供了多种不同的数据库引擎&#xff0c;其中最常见的有 MyISAM、InnoDB、MEMORY 和 BLACKHOLE。这四个引擎分别有以下特点&#xff1a; 1. MyISAM MyISAM 是 MySQL 的默认引擎。它对于只有较少的修改、大量读取的应用场景具有良好的性能。它不支持事务处理&#xff0c;也…

理解字符串常量池(JVM)

大纲 思考 如何查看字符串常量池&#xff08;StringTable&#xff09;&#xff1f; 使用 jclasslib 插件打开字节码&#xff0c;选择 常量池 -> 显示所选 -> CONSTANT_String_info&#xff0c;左侧过滤后的内容即为字符串常量池 字符串常量池、方法区、永久代和元空间的…

Dynamic Wallpaper for Mac:动态壁纸让桌面更生动

Dynamic Wallpaper for Mac是一款为苹果电脑用户精心设计的动态壁纸软件&#xff0c;它以其丰富的功能和精美的壁纸库&#xff0c;为用户带来了更加生动和个性化的桌面体验。 Dynamic Wallpaper for Mac v17.8中文版下载 这款软件支持多种动态壁纸&#xff0c;用户可以根据自己…

unity学习(86)——细节优化

东西已经做出来了&#xff0c;现在需要的是优化&#xff0c;说得简单&#xff0c;做起来难。 1.122包的优化&#xff0c;避免重复创建&#xff01; 2.为何会出现一边动&#xff0c;一边不动的情况。重复登录后依旧是unity可以看到移动&#xff0c;但是exe那边看不到移动&#…

数据结构PT1——线性表/链表

1&#xff1a;顺序存储实现(数组实现) Data&#xff1a; a1 a2 .....ai ai1 .... an .... typedef struct LNode *List; //指向LNode的指针&#xff0c;这是typedef的&#xff0c;你可以随时声明&#xff0c;而不加typedef只是创建一个 struct LNode{ //结构体成员ElementT…

Vue.js------Vue组件基础

能够理解Vue组件概念和作用能够掌握封装创建组件能力能够使用组件之间通信能够完成todo案例 一.Vue组件创建和使用 1.折叠面板-实现多个 创建一个文件夹demo 具体步骤请参考vue.js---vue基础 ⚫ 解决方案: 采用vue提供的单.vue文件-组件方式来封装一套然后复用 在component…

Jackson 2.x 系列【24】Spring Web 集成

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Jackson 版本 2.17.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-jaskson-demo 文章目录 1. 前言2. Spring Web3. Jackson2ObjectMapperBuilder4. Jackson2ObjectMapperFa…

探索传感器世界:类型与应用详解

传感器是一种能感知并测量特定物理量、化学量或其他参数&#xff0c;并将其转换为可供处理、记录或控制的电信号的装置。 物联网传感器设备种类繁多&#xff0c;以下是一些常见的类型&#xff1a; 一、 温度传感器 1、热电阻温度传感器&#xff1a;利用金属的电阻随温度变化的…
最新文章