OpenCvSharp学习笔记6--改变图像的对比度和亮度

目的

访问像素值mat.At<T>(y,x)

用0初始化矩阵Mat.Zeros

饱和操作SaturateCast.ToByte

亮度和对比度调整

g(x)=αf(x)+β

用α(>0)和β一般称作增益(gain)和偏置(bias),分别控制对比度和亮度

把f(x)看成源图像像素,把g(x)看成输出图像像素

g(i,j)=α⋅f(i,j)+β

其中,i和j表示像素位于 第i行 和 第j列(左上角为第0行、第0列)

相关函数

Mat.Zeros:初始一个所有值为0的矩阵

Mat new_Image1 = Mat.Zeros(image.Size(), image.Type());

SaturateCast.ToByte:饱和操作,当<0时,返回0,当>255时,返回255,其它返回原值

return SaturateCast.ToByte(alpha * source + beta);

Mat.ConvertTo: g(i,j)=α⋅f(i,j)+β 转化

image.ConvertTo(new_Image2, image.Type(), alpha, beta);

SaturateCast.ToByte与ConvertTo的差异

使用SaturateCast.ToByte与ConvertTo操作的结果不完全一致。

如计算85*0.7+0.0=59.499999999999993D, SaturateCast.ToByte的结果为59,而ConvertTo的结果为60。

图像示例

α=2.2 and β=80时的效果图

α、β参数的意义

官网源图

β越大,图像越亮,直方图往右

使用前面的公式g(i,j)=α⋅f(i,j)+β对图像进行亮度调整,当α=1,β=80时

对比原图(左)与调整后(右)的直方图,调整亮度后,直方图整体往右压缩。因为饱和操作的结果是,大于255的值设为255(为对比显示效果,255的个数没有显示完全)。(直方图:往左变暗,往右变亮)

α<1时,对比度变小

当β相同80,左边α=1,右边α=0.5,对比可知,当α<1时,图像色阶变小,对比度变小。

(左边α=1,右边α=0.5)

结论:调大β,可以变亮,因为对比度的变小导致有轻微蒙纱。调小α,可有效改善,但会损失原来明亮部分的细节。

伽马校正

伽马校正可用于亮度的非性调整。公式如下:

对比不同的像素的输入与输出值(图片来源于OpenCV官网)

下图为原图与α=1.3,β=40时的对比

结论:图像是调亮了,但天空的细节少了

下图为:原图与gamma=0.4时的对比

图像调亮了,细节也在。

下图为,原图(左)、α=1.3,β=40调整(中)及gamma=0.4(右)调整的直方图对比

对起简单的线性对比度和亮度调整,伽马校正效果更优。

源码示例

Mat src;//源图

string winName = "Brightness and contrast adjustments";

int alpha = 100;
int beta = 255;

string gammaWinName = "Gamma correction";

Mat saturateCastHistImg;//SaturateCast后的图像直方图

double maxVal = -1;//源图最多像素点个数

public void Run(ParamBase paramBase) {

    //src = Cv2.ImRead(ImagePath.Lena);
    src = Cv2.ImRead(ImagePath.Underexposed);
    if (src.Empty()) throw new Exception("图像打开有误");
    //原图像的直方图
    GetHistResult(src, out _, out Mat histImg);
    Cv2.ImShow("Hist:Original", histImg);

    Cv2.NamedWindow(winName, WindowFlags.AutoSize);
    Cv2.CreateTrackbar("α=n/100", winName, ref alpha, 500, alphaOnChange);
    Cv2.CreateTrackbar("β=n-255", winName, ref beta, 500, betaOnChange);

    //Cv2.ImShow("Original Image", image);

    Cv2.NamedWindow(gammaWinName, WindowFlags.AutoSize);
    Cv2.CreateTrackbar("γ= n/100", gammaWinName, 200, gammaOnChange);
    Cv2.SetTrackbarPos("γ= n/100", gammaWinName, 100);

    Cv2.WaitKey();
    Cv2.DestroyAllWindows();
}
#region 滚动条Bug,多次滚动后,ref value的值与pos不同步
private void alphaOnChange(int pos, IntPtr userdata) {
    alpha = pos;
    OnChange();
}
private void betaOnChange(int pos, IntPtr userdata) {
    beta = pos;
    OnChange();
}
#endregion

private void OnChange() {
    //对比度
    double alphaD = alpha / 100.0D;
    //亮度
    double betaD = beta - 255;
    //初始化所有值都为0的矩阵
    Mat new_Image1 = Mat.Zeros(src.Size(), src.Type());

    //使用公式:g(i,j)=α⋅f(i,j)+β
    //方式一
    for (int row = 0; row < src.Rows; row++) {
        for (int col = 0; col < src.Cols; col++) {
            var val = src.At<Vec3b>(row, col);//访问图像像素
            Vec3b newVal = new Vec3b();
            for (int c = 0; c < src.Channels(); c++) {
                newVal[c] = Adjust(val[c], alphaD, betaD);
            }
            new_Image1.At<Vec3b>(row, col) = newVal;
        }
    }
    //方式二
    Mat new_Image2 = Mat.Zeros(src.Size(), src.Type());
    src.ConvertTo(new_Image2, src.Type(), alphaD, betaD);

    //比较 SaturateCast.ToByte与 ConvertTo的结果是否一样
    using var difMat = new Mat();
    Cv2.Absdiff(new_Image1, new_Image2, difMat);
    if (difMat.Split().Any(z => z.CountNonZero() > 0)) {
        //有差异
        PutText(new_Image2, "Different", new Point(5, 30));
        //注意 85*0.7 =     59.499999999999993    double
    }

    GetHistResult(new_Image1, out _, out saturateCastHistImg);
    var adjust = $"alpha={alphaD.ToString("0.00")},beta={betaD}";
    PutText(saturateCastHistImg, adjust);
    Cv2.ImShow("Hist,SaturateCast", saturateCastHistImg);


    PutText(new_Image1, adjust);
    Cv2.HConcat(src, new_Image1, new_Image1);

    PutText(new_Image2, $"alpha={alphaD.ToString("0.00")},beta={betaD}");
    Cv2.HConcat(src, new_Image2, new_Image2);
    Cv2.ImShow(winName, new_Image1);
    Cv2.ImShow("ConvertTo", new_Image2);
}

/// <summary>
/// 调整对比度和亮度
/// </summary>
/// <param name="source">源值</param>
/// <param name="alpha">对比度</param>
/// <param name="beta">亮度</param>
/// <returns></returns>
private byte Adjust(byte source, double alpha, double beta) {
    //饱和操作,当<0时,返回0,当>255时,返回255,其它返回原值
    return SaturateCast.ToByte(alpha * source + beta);
}

#region 直方图相关
/// <summary>
/// 计算并生成绘制直方图
/// </summary>
/// <param name="src">待统计的图像</param>
/// <param name="hist">直方图结果</param>
/// <param name="histImage">直方图的绘制结果</param>
private void GetHistResult(Mat src, out Mat hist, out Mat histImage) {

    hist = new Mat();
    const int histW = 512;
    const int histH = 400;
    histImage = new Mat(histH, histW, MatType.CV_8UC3, Scalar.All(0));

    int histSize = 256;//直方图数组大小
    var range = new Rangef(0, 256);//统计0至255(=266-1)
    //将图像像素灰度[0,255]共分为histSize个等级统计,
    for (int channel = 0; channel < src.Channels(); channel++) {
        Cv2.CalcHist(images: new[] { src },//待统计的图像
                    channels: new[] { channel },//待统计的通道
                    mask: null,//掩膜
                    hist: hist,//输出的统计结果
                    dims: 1,//直方图维度
                    histSize: new[] { histSize },//将range分为histSize梯度
                    ranges: new[] { range });//待统计通道像素的范围,不在这个范围内的不统计
        DrawHist(histImage, hist, (channel == 0 ? Scalar.Blue : (channel == 1 ? Scalar.Green : Scalar.Red)));
    }
}

/// <summary>
/// 绘制直方图
/// </summary>
/// <param name="histImage">直方图绘制结果</param>
/// <param name="histSize">直方图数组大小</param>
/// <param name="color">线的颜色</param>
private void DrawHist(Mat histImage, Mat hist, Scalar color) {
    var binW = Math.Round((double)histImage.Width / hist.Height);
    if (maxVal > 0) {
        //截断超过源图最大值的像素大数(防止饱和像素过多)
        Cv2.Threshold(hist, hist, maxVal, maxVal, ThresholdTypes.Trunc);
    }
    //归一化
    Cv2.Normalize(hist, hist, 0, histImage.Rows, NormTypes.MinMax, -1);
    for (int i = 1; i < hist.Height; i++) {
        var pt1 = new Point2d(binW * (i - 1), histImage.Height - Math.Round(hist.At<float>(i - 1)));
        var pt2 = new Point2d(binW * (i), histImage.Height - Math.Round(hist.At<float>(i)));
        //OpenCvSharp有Bug?有时会报错
        Cv2.Line(histImage, (Point)pt1, (Point)pt2, color, 1, LineTypes.AntiAlias);
    }
}
#endregion

#region Gamma矫正
private Mat GammaCorrection(Mat src,double gamma) {
    var lookUpTable = new Mat(new Size(1, 256), MatType.CV_8U);
    for (int i = 0; i < 256; i++) {
        lookUpTable.At<byte>(0, i) = SaturateCast.ToByte(Math.Pow(i / 255.0D, gamma) * 255.0D);
    }
    Mat dst = new Mat();
    //查表法,性能优化
    Cv2.LUT(src, lookUpTable, dst);
    return dst;
}

private void gammaOnChange(int pos,IntPtr userdata) {
    double gamma = pos / 100.0D;
    using var dst = GammaCorrection(src, gamma);
    GetHistResult(dst, out _, out Mat histImg);

    var adjust = $"gamma={gamma.ToString("0.00")}";
    PutText(histImg,adjust);
    Cv2.ImShow("Hist,Gamma Correction", histImg);

    PutText(dst, adjust);
    //左右合并
    Cv2.HConcat(src, dst, dst);
    Cv2.ImShow(gammaWinName, dst);
}
#endregion

private void PutText(Mat src, string text) {
    PutText(src, text, new Point(5, 15));
}
private void PutText(Mat src, string text, Point point) {
    Cv2.PutText(src, text, point, HersheyFonts.HersheySimplex, 0.5, Scalar.Red);
}

参考

https://docs.opencv.org/4.7.0/d3/dc1/tutorial_basic_linear_transform.html

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图南科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值