本文主要是记录个人的开发过程,及过程中遇到的问题及解决方法,对个人工作的一个总结。

一、HDR图像合成

由不同曝光设置拍摄的多张图像创建高动态范围High Dynamic Range(HDR)图像。
使用的是c++和OpenCV4.x。

主程序主要参考对象:使用 OpenCV 进行高动态范围(HDR)成像

1.捕获不同曝光度的多张图像

使用相机拍摄三张曝光时间不同的图像

  1. 曝光不足的图像:该图像比正确曝光的图像更暗。 目标是捕捉非常明亮的图像部分。
  2. 正确曝光的图像:这是相机将根据其估计的照明拍摄的常规图像。
  3. 曝光过度的图像:该图像比正确曝光的图像更亮。 目标是拍摄非常黑暗的图像部分。

如果场景的动态范围很大,我们可以拍摄三张以上的图片来合成 HDR 图像。

读取不同曝光时间的图像:

void readImagesAndTimes(vector<Mat> &images, vector<float> &times)      
{
  
  int numImages = 3;
  
  static const float timesArray[] = { 0.000038, 0.000304, 0.003432 };    // 单位秒
  times.assign(timesArray, timesArray + numImages);         // 将timesArray里的值全部赋给times
  
  static const char* filenames[] = { "./pic/welding/10013_2_38.jpg",  "./pic/welding/10013_0_304.jpg", "./pic/welding/10013_1_3432.jpg" };
  for(int i=0; i < numImages; i++)
  {
    Mat im = imread(filenames[i]);  //默认读取BGR格式
    images.push_back(im);   //全部存储在images
  }
}

2.对齐图像

合成 HDR 图像时使用的图像如果未对齐可能会导致严重的伪影。

OpenCV 提供了一种简单的方法,使用 AlignMTB 对齐这些图像。 该算法将所有图像转换为中值阈值位图median threshold bitmaps(MTB)。 图像的 MTB 生成方式为将比中值亮度的更亮的分配为 1,其余为 0。 MTB 不随曝光时间的改变而改变。 因此不需要我们指定曝光时间就可以对齐 MTB。

  // Align input images 中值阈值位图MTB方式 对齐图片
  cout << "Aligning images ... " << endl;
  Ptr<AlignMTB> alignMTB = createAlignMTB();
  alignMTB->process(images, images);

3.提取相机响应函数

典型相机的响应与场景亮度不成线性关系。

假设有两个物体由同一个相机拍摄,在现实世界中其中一个物体是另一个物体亮度的两倍。 当您测量照片中两个物体的像素亮度时,较亮物体的像素值将不会是较暗物体的两倍。 在不估计相机响应函数Camera Response Function(CRF)的情况下,我们将无法将图像合并到一个HDR图像中。

如果我们知道每个图像的曝光时间,则可以从图像估计 CRF。

使用 OpenCV 的 CalibrateDebevec 或者 CalibrateRobertson 就可以用 2 行代码找到 CRF。本篇使用 CalibrateDebevec。

// Obtain Camera Response Function (CRF)  提取相机响应函数
cout << "Calculating Camera Response Function (CRF) ... " << endl;
Mat responseDebevec;
Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
calibrateDebevec->process(images, responseDebevec, times);

4.合并图像

一旦 CRF 评估结束,可以曝光图像合并成一个HDR图像。
分别使用了两种方法合成HDR图像。

  • Debevec方法合并图像
  • 输入图片、曝光时间、响应函数
// Debevec方法合并图像
Mat Merging_Debevec(vector<Mat>& images, vector<float>& times, Mat& responseDebevec)    
{
    cout << "Merging images into one HDR image ... " << endl;
    Mat hdrDebevec;
    Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();
    mergeDebevec->process(images, hdrDebevec, times, responseDebevec);
    // Save HDR image.    保存图像
    imwrite("./pic/welding/hdrDebevec.hdr", hdrDebevec);
    cout << "saved hdrDebevec.hdr " << endl;
    return hdrDebevec;
}

// Robertson方法合并图像
Mat Merging_Robertson(vector<Mat>& images, vector<float>& times, Mat& responseDebevec)     
{
    Mat hdrRobertson;
    Ptr<MergeRobertson> mergeRobertson = createMergeRobertson();
    mergeRobertson->process(images, hdrRobertson, times, responseDebevec);
    // Save HDR image.    保存图像
    imwrite("./pic/welding/hdrRobertson.hdr", hdrRobertson);
    cout << "saved hdrRobertson.hdr " << endl;
    return hdrRobertson;
}

保存的 HDR 图像可以在 Photoshop 中加载并进行色调映射。

5.色调映射

将高动态范围(HDR)图像转换为 8 位单通道图像的过程称为色调映射。这个过程的同时还需要保留尽可能多的细节。

有几种色调映射算法,OpenCV 实现了其中的四个。

没有一个绝对正确的方法来做色调映射。 通常,我们希望在色调映射图像中看到比任何一个曝光图像更多的细节。 有时色调映射的目标是产生逼真的图像,而且往往是产生超现实图像的目标。 在 OpenCV 中实现的算法倾向于产生现实的并不那么生动的结果。

来看看各种选项。 以下列出了不同色调映射算法的一些常见参数。

  1. 伽马gamma:该参数通过应用伽马校正来压缩动态范围。 当伽马等于 1 时,不应用修正。 小于 1 的伽玛会使图像变暗,而大于 1 的伽马会使图像变亮。
  2. 饱和度saturation:该参数用于增加或减少饱和度。 饱和度高时,色彩更丰富,更浓。 饱和度值接近零,使颜色逐渐消失为灰度。
  3. 对比度contrast:控制输出图像的对比度(即 log(maxPixelValue/minPixelValue))。

(1)Drago 色调映射

Drago 色调映射的参数如下

createTonemapDrago(
		float gamma = 1.0f,
		float saturation = 1.0f,
		float bias = 0.85f
)

bias 是 [0, 1] 范围内偏差函数的值。 从 0.7 到 0.9 的值通常效果较好。 默认值是 0.85。

下面是使用方式。
最后的结果乘以 3 只是因为它给出了最令人满意的结果。

// Drago算法色调映射
Mat Tonemap_DDrago(Mat& hdr)
{
    cout << "Tonemaping using Drago's method ... " << endl;
    Mat ldrDDrago;
    Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);
    tonemapDrago->process(hdr, ldrDDrago);
    ldrDDrago = 3 * ldrDDrago;
    imwrite("./pic/welding/ldrD-Drago.jpg", ldrDDrago * 255);
    cout << "saved ldr-Drago.jpg" << endl;
    return ldrDDrago;
}

(2)Durand 色调映射

该算法需付费,这里没有使用。

算法参数及使用在参考文章有。

(3)Reinhard 色调映射

Reinhard 色调映射的参数如下所示。

createTonemapReinhard(
		float gamma = 1.0f,
		float intensity = 0.0f,
		float light_adapt = 1.0f,
		float color_adapt = 0.0f
)

参数说明:

  1. intensity 参数应在 [-8, 8] 范围内。 更高的亮度值会产生更明亮的结果。
  2. light_adapt 控制灯光,范围为 [0, 1]。 值 1 表示仅基于像素值的自适应,而值 0 表示全局自适应。 中间值可以用于两者的加权组合。
  3. 参数 color_adapt 控制色彩,范围为 [0, 1]。 如果值被设置为 1,则通道被独立处理,如果该值被设置为 0,则每个通道的适应级别相同。中间值可以用于两者的加权组合。

下面是使用方式。

// Reinhard算法色调映射
Mat Tonemap_DReinhard(Mat& hdr)
{
    cout << "Tonemaping using Reinhard's method ... " << endl;
    Mat ldrDReinhard;
    Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0, 0, 0);
    tonemapReinhard->process(hdr, ldrDReinhard);
    imwrite("./pic/welding/ldrD-Reinhard.jpg", ldrDReinhard * 255);
    cout << "saved ldr-Reinhard.jpg" << endl;
    return ldrDReinhard;
}

(4)Mantiuk 色调映射

Mantiuk 色调映射的参数如下所示。

createTonemapMantiuk
(
float gamma = 1.0f,
float scale = 0.7f,
float saturation = 1.0f
)

参数 scale 是对比度比例因子。 从 0.7 到 0.9 的值通常效果较好。

下面是使用方式。

// Mantiuk算法色调映射
Mat Tonemap_Mantiuk(Mat& hdr)
{
    cout << "Tonemaping using Mantiuk's method ... " << endl;
    Mat ldrDMantiuk;
    Ptr<TonemapMantiuk> tonemapMantiuk = createTonemapMantiuk(2.2, 0.85, 1.2);
    tonemapMantiuk->process(hdr, ldrDMantiuk);
    ldrDMantiuk = 3 * ldrDMantiuk;
    imwrite("./pic/welding/ldrD-Mantiuk.jpg", ldrDMantiuk * 255);
    cout << "saved ldr-Mantiuk.jpg" << endl;
    return ldrDMantiuk;
}

二、边缘检测

主要参考文章:
OpenCV4(C++)—— 边缘检测的使用:使用方法
OpenCV数字图像处理基于C++:边缘检测:原理、步骤、手工实现及函数实现
【OpenCV(C++)】图像变换:边缘检测:本文四种算子函数的独立使用。

还有一些算子函数没有使用到,如Prewitt 算子、LoG算子(也就是Laplace of Gaussian function(高斯拉普拉斯函数))、

后续可以补充。(遥遥无期)

1. Sobel算子

Sobel算法(索贝尔算子)是一种用于边缘检测的离散微分算子,它结合了高斯平滑和微分求导。该算子用于计算图像明暗程度近似值,根据图像边缘旁边明暗程度把该区域内超过某个数的特定点记为边缘。

函数定义如下。

void cv::Sobel(
    InputArray src,   	// 输入图像,可以是单通道(灰度图)或多通道(彩色图)
    OutputArray dst,  	// 输出图像,与输入图像具有相同的尺寸和深度
    int ddepth,       	// 输出图像的深度,使用-1表示输出与输入图像具有相同的深度(也可写CV_16S)
    int dx, int dy,   	// x方向和y方向上的导数阶数,0表示只计算横向导数,1表示只计算纵向导数
    int ksize = 3,    	// Sobel核的大小,必须为1、3、5或7。默认值为3
    double scale = 1, 	// 缩放因子,默认为1,将应用于计算得到的导数结果
    double delta = 0  	// 可选的增量,默认为0,将添加到最终结果中
)

第三个参数也可理解为输出图像的类型。由于梯度计算涉及差分运算,可能会产生负数值和超过 255 的正数值
故要使用有符号的 CV_16S 作为输出图像的深度参数,确保导数结果的范围足够大。

后续在需要显示或处理这些导数结果时,使用 cv::convertScaleAbs 函数将其转换为无符号 8 位整数(CV_8U)类型,便于显示和处理。

函数使用方式如下。

// Sobel算子边缘检测
Mat Sobel_detecte(Mat& image)
{
    // Sobel算子检测
    Mat sobelX, sobelY;
    Sobel(image, sobelX, CV_32F, 1, 0);
    Sobel(image, sobelY, CV_32F, 0, 1);

    // 转换为绝对值
    Mat sobelXAbs, sobelYAbs;
    convertScaleAbs(sobelX, sobelXAbs, 30);         // 后面的参数是倍数,可以调
    convertScaleAbs(sobelY, sobelYAbs, 30);
    /*
    * convertScaleAbs(输入数组, 输出数组, 乘数因子, 偏移量)
    */
    
    // 整幅图的一阶边缘
    cv::Mat edges;
    edges = sobelXAbs + sobelYAbs;
    return edges;
}

2. Scharr算子

Scharr算子与Sobel算子的参数是一样的,两者主要是X和Y方向的滤波器(卷积核)不同。Scharr算子可以看成Sobel算子的加强版,对一些导数较小(即像素值差距不大)的弱边缘仍然可以检测出。

函数使用方式如下。

// Scharr算子边缘检测
Mat Scharr_detecte(Mat& image)
{
    // Scharr算子检测
    Mat ScharrX, ScharrY;
    Scharr(image, ScharrX, CV_32F, 1, 0);
    Scharr(image, ScharrY, CV_32F, 0, 1);
    // 转换为绝对值
    Mat ScharrXAbs, ScharrYAbs;
    convertScaleAbs(ScharrX, ScharrXAbs, 10);       // // 后面的参数是倍数,可以调
    convertScaleAbs(ScharrY, ScharrYAbs, 10);
    // 整幅图的一阶边缘
    cv::Mat edges;
    edges = ScharrXAbs + ScharrYAbs;
    return edges;
}

3. Laplacian算子

Laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取。Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。

函数定义如下。

void cv::Laplacian(
    InputArray src,     // 输入图像,可以是单通道(灰度图)或多通道(彩色图)
    OutputArray dst,    // 输出图像,与输入图像具有相同的尺寸和深度
    int ddepth,         // 输出图像的深度,使用-1表示输出与输入图像具有相同的深度
    int ksize = 1,      // 拉普拉斯核的大小,可以设置为1、3、5或7。默认值为1,但常用为3
    double scale = 1,   // 缩放因子,默认为1,将应用于计算得到的拉普拉斯结果
    double delta = 0,   // 可选的增量,默认为0,将添加到最终结果中
    int borderType = cv::BORDER_DEFAULT // 边界类型,默认为cv::BORDER_DEFAULT
);

函数使用方式如下。

// Laplacian算子边缘检测
Mat Laplacian_detecte(Mat& image)
{
    Mat Gresult, edges1, edges2;
    GaussianBlur(image, Gresult, cv::Size(3, 3), 5, 0);  // 进行高斯滤波
    imwrite("./pic/welding/Gaussian_result.jpg", Gresult * 255);      // 保存一张高斯滤波的图片,*255是因为输入的图片偏灰度图,转换完后值太小,直接保存会被归一化
    Laplacian(Gresult, edges1, -1, 5, 1, 0);
    convertScaleAbs(edges1, edges2, 20);      // 转换为绝对值
    return edges2;
}

4. Canny算法

canny边缘检测算子是一个多级边缘检测算法。

最优边缘检测的评价标准:低错误率、高定位性、最小响应。为了满足这些要求,Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测用4个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。

Canny算法的流程较为复杂,详细步骤这里按下不表,可以参考1参考2。在OpenCV4中提供了Canny()函数用于实现Canny算法检测图像中的边缘,极大的简化了使用Canny算法提取边缘信息的过程。

函数定义如下。

void cv::Canny(
    InputArray image,     // 输入图像,单通道灰度图像
    OutputArray edges,    // 输出边缘图像,二值图像,非零像素表示边缘
    double threshold1,    // 第一个阈值,用于边缘链接的低阈值
    double threshold2,    // 第二个阈值,用于边缘链接的高阈值
    int apertureSize = 3, // Sobel 算子的孔径大小,默认为 3
    bool L2gradient = false // 是否使用 L2 梯度计算,默认为 false,即使用 L1 梯度计算
);

一般情况下,较大阈值与较小阈值的比值在2:1到3:1之间。

函数使用方式如下。

// Canny算子边缘检测
void Canny_detecte(Mat& image, Mat& edges1, Mat& edges2)
{
    Mat Gresult, edges;
    GaussianBlur(image, Gresult, cv::Size(5, 5), 5, 5);  // 进行高斯滤波
    Gresult.convertTo(edges, CV_8U, 255);       // 将输入图像从CV_32F转换为CV_8U,不转化的话下面的canny函数运行时可能会有数据溢出的情况
    // canny 调整第3、4个参数,得到不同效果 
    Canny(edges, edges1, 500, 1000, 5);     // 小阈值  
    Canny(edges, edges2, 1200, 2400, 5);    // 大阈值
}

三、整体代码

#include "opencv2/photo.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <vector>
#include <iostream>
#include <fstream>

using namespace cv;
using namespace std;

/*
* 运行前修改:
* readImagesAndTimes函数中的图片文件路径和曝光时间
* 合并图像函数中的保存路径
* 色调映射函数中的保存路径
*/

/*
* 读取图片和曝光时间
*/
void readImagesAndTimes(vector<Mat> &images, vector<float> &times)      
{
  
  int numImages = 3;
  
  static const float timesArray[] = { 0.000038, 0.000304, 0.003432 };    // 单位秒
  times.assign(timesArray, timesArray + numImages);         // 将timesArray里的值全部赋给times
  
  static const char* filenames[] = { "./pic/welding/10013_2_38.jpg", "./pic/welding/10013_0_304.jpg", "./pic/welding/10013_1_3432.jpg" };
  for(int i=0; i < numImages; i++)
  {
    Mat im = imread(filenames[i]);  //默认读取BGR格式
    images.push_back(im);   //全部存储在images
  }

}

/****************合并图像********************/
/*
* Debevec方法合并图像
* 输入图片、曝光时间、响应函数
*/
Mat Merging_Debevec(vector<Mat>& images, vector<float>& times, Mat& responseDebevec)    
{
    cout << "Merging images into one HDR image ... " << endl;
    Mat hdrDebevec;
    Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();
    mergeDebevec->process(images, hdrDebevec, times, responseDebevec);
    // Save HDR image.    保存图像
    imwrite("./pic/welding/hdrDebevec.hdr", hdrDebevec);
    cout << "saved hdrDebevec.hdr " << endl;
    return hdrDebevec;
}

/*
* Robertson方法合并图像
* 输入图片、曝光时间、响应函数
*/
Mat Merging_Robertson(vector<Mat>& images, vector<float>& times, Mat& responseDebevec)     
{
    Mat hdrRobertson;
    Ptr<MergeRobertson> mergeRobertson = createMergeRobertson();
    mergeRobertson->process(images, hdrRobertson, times, responseDebevec);
    // Save HDR image.    保存图像
    imwrite("./pic/welding/hdrRobertson.hdr", hdrRobertson);
    cout << "saved hdrRobertson.hdr " << endl;
    return hdrRobertson;
}


/******************色调映射****************/
/*
* Drago算法色调映射
* 输入Debevec方法的合并图像
*/
Mat Tonemap_DDrago(Mat& hdr)
{
    cout << "Tonemaping using Drago's method ... " << endl;
    Mat ldrDDrago;
    Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);
    tonemapDrago->process(hdr, ldrDDrago);
    ldrDDrago = 3 * ldrDDrago;
    imwrite("./pic/welding/ldrD-Drago.jpg", ldrDDrago * 255);
    cout << "saved ldr-Drago.jpg" << endl;
    return ldrDDrago;
}

/*
* Reinhard算法色调映射
* 输入Debevec方法的合并图像
*/
Mat Tonemap_DReinhard(Mat& hdr)
{
    cout << "Tonemaping using Reinhard's method ... " << endl;
    Mat ldrDReinhard;
    Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0, 0, 0);
    tonemapReinhard->process(hdr, ldrDReinhard);
    imwrite("./pic/welding/ldrD-Reinhard.jpg", ldrDReinhard * 255);
    cout << "saved ldr-Reinhard.jpg" << endl;
    return ldrDReinhard;
}

/*
* Mantiuk算法色调映射
* 输入Debevec方法的合并图像
*/
Mat Tonemap_Mantiuk(Mat& hdr)
{
    cout << "Tonemaping using Mantiuk's method ... " << endl;
    Mat ldrDMantiuk;
    Ptr<TonemapMantiuk> tonemapMantiuk = createTonemapMantiuk(2.2, 0.85, 1.2);
    tonemapMantiuk->process(hdr, ldrDMantiuk);
    ldrDMantiuk = 3 * ldrDMantiuk;
    imwrite("./pic/welding/ldrD-Mantiuk.jpg", ldrDMantiuk * 255);
    cout << "saved ldr-Mantiuk.jpg" << endl;
    return ldrDMantiuk;
}


/***************边缘检测*****************/
/*
* Sobel算子边缘检测
*/
Mat Sobel_detecte(Mat& image)
{
    // Sobel算子检测
    Mat sobelX, sobelY;
    Sobel(image, sobelX, CV_32F, 1, 0);
    Sobel(image, sobelY, CV_32F, 0, 1);

    // 转换为绝对值
    Mat sobelXAbs, sobelYAbs;
    convertScaleAbs(sobelX, sobelXAbs, 30);         // 后面的参数是倍数,可以调
    convertScaleAbs(sobelY, sobelYAbs, 30);
    /*
    * convertScaleAbs(输入数组, 输出数组, 乘数因子, 偏移量)
    */

    // 整幅图的一阶边缘
    cv::Mat edges;
    edges = sobelXAbs + sobelYAbs;
    return edges;
}

/*
* Scharr算子边缘检测
*/
Mat Scharr_detecte(Mat& image)
{
    // Scharr算子检测
    Mat ScharrX, ScharrY;
    Scharr(image, ScharrX, CV_32F, 1, 0);
    Scharr(image, ScharrY, CV_32F, 0, 1);

    // 转换为绝对值
    Mat ScharrXAbs, ScharrYAbs;
    convertScaleAbs(ScharrX, ScharrXAbs, 10);       // // 后面的参数是倍数,可以调
    convertScaleAbs(ScharrY, ScharrYAbs, 10);

    // 整幅图的一阶边缘
    cv::Mat edges;
    edges = ScharrXAbs + ScharrYAbs;
    return edges;
}

/*
* Laplacian算子边缘检测
*/
Mat Laplacian_detecte(Mat& image)
{
    Mat Gresult, edges1, edges2;
    GaussianBlur(image, Gresult, cv::Size(3, 3), 5, 0);  // 进行高斯滤波
    imwrite("./pic/welding/Gaussian_result.jpg", Gresult * 255);      // 保存一张高斯滤波的图片
    Laplacian(Gresult, edges1, -1, 5, 1, 0);
    convertScaleAbs(edges1, edges2, 20);      // 转换为绝对值
    return edges2;
}

/*
* Canny算子边缘检测
*/
void Canny_detecte(Mat& image, Mat& edges1, Mat& edges2)
{
    Mat Gresult, edges;
    GaussianBlur(image, Gresult, cv::Size(5, 5), 5, 5);  // 进行高斯滤波
    Gresult.convertTo(edges, CV_8U, 255);       // 将输入图像从CV_32F转换为CV_8U
    // canny 调整第3、4个参数,得到不同效果 
    Canny(edges, edges1, 500, 1000, 5);     // 小阈值  
    Canny(edges, edges2, 1200, 2400, 5);    // 大阈值
}





int main(int, char**argv)
{
  // Read images and exposure times 读入图片和曝光时间
  cout << "Reading images ... " << endl;
  vector<Mat> images;
  vector<float> times;
  readImagesAndTimes(images, times);
  
  
  // Align input images 中值阈值位图MTB方式 对齐图片
  cout << "Aligning images ... " << endl;
  Ptr<AlignMTB> alignMTB = createAlignMTB();
  alignMTB->process(images, images);
  
  // Obtain Camera Response Function (CRF)  提取相机响应函数
  cout << "Calculating Camera Response Function (CRF) ... " << endl;
  Mat responseDebevec;
  Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
  calibrateDebevec->process(images, responseDebevec, times);
  

  /***************图像合成******************/

  // Merge images into an HDR linear image  Debevec方法合并图像
  Mat hdrDebevec;
  hdrDebevec = Merging_Debevec(images, times, responseDebevec);
  
  // Merge images into an HDR linear image  Robertson方法合并图像
  Mat hdrRobertson;
  hdrRobertson = Merging_Robertson(images, times, responseDebevec);


  /****************色调映射*****************/

  // Tonemap using Drago's method to obtain 24-bit color image      使用Drago算法色调映射获得24位彩色图像
  Mat ldrDDrago;
  ldrDDrago = Tonemap_DDrago(hdrDebevec);
  
  // Tonemap using Durand's method obtain 24-bit color image    使用Durand算法色调映射获得24位彩色图像
  // 付费内容
  /*cout << "Tonemaping using Durand's method ... ";
  Mat ldrDurand;
  Ptr<cv::TonemapDurand> tonemapDurand = createTonemapDurand(1.5,4,1.0,1,1);
  tonemapDurand->process(hdrDebevec, ldrDurand);
  ldrDurand = 3 * ldrDurand;
  imwrite("ldr-Durand.jpg", ldrDurand * 255);
  cout << "saved ldr-Durand.jpg"<< endl;*/
  
  // Tonemap using Reinhard's method to obtain 24-bit color image   使用Reinhard色调映射算法获得24位彩色图像
  Mat ldrDReinhard;
  ldrDReinhard = Tonemap_DReinhard(hdrDebevec);

  
  // Tonemap using Mantiuk's method to obtain 24-bit color image       使用Mantiuk色调映射算法获得24位彩色图像
  Mat ldrDMantiuk;
  ldrDMantiuk = Tonemap_Mantiuk(hdrDebevec);

  cout << "色调映射完成!" << endl;
  
  //Mat test02 = imread("test02.png");
  Mat test01 = ldrDMantiuk;
  cout << "开始边缘检测!" << endl;
  
  // Sobel 算子检测
  Mat Sobel_img;
  Sobel_img = Sobel_detecte(test01);
  imwrite("./pic/welding/Sobel_img.jpg", Sobel_img);
  cout << "saved Sobel_img.jpg" << endl;
  
  // Scharr算子检测
  Mat Scharr_img;  
  Scharr_img = Scharr_detecte(test01);
  imwrite("./pic/welding/Scharr_img.jpg", Scharr_img);
  cout << "saved Scharr_img.jpg" << endl;

  // Laplacian算子检测
  Mat Laplacian_img;
  Laplacian_img = Laplacian_detecte(test01);
  imwrite("./pic/welding/Laplacian_img.jpg", Laplacian_img);
  cout << "saved Laplacian_img.jpg" << endl;

  // Canny算子检测
  Mat Canny_imgL, Canny_imgB;
  Canny_detecte(test01, Canny_imgL, Canny_imgB);
  imwrite("./pic/welding/Canny_imgL.jpg", Canny_imgL);
  imwrite("./pic/welding/Canny_imgR.jpg", Canny_imgB);
  cout << "saved Canny_imgL.jpg and Canny_imgR.jpg" << endl;

  ofstream responseTxt;
  responseTxt.open("./pic/welding/responseDebevec.txt",ios::out);
  responseTxt << " 相机响应函数responseDebevec:" << endl << responseDebevec << endl;
  responseTxt.close();
  //cout << " 相机响应函数responseDebevec:" << endl << responseDebevec << endl;

  getchar();
  return EXIT_SUCCESS;
}

四、过程中遇到的问题及解决

1. 保存图片时全黑

问题:在保存高斯滤波图片时,程序运行过程中可以看到图片时有内容的,但保存后再打开却一片黑。

查看图像矩阵Mat时发现其值都比较小,在保存后貌似像素值被标准化还是归一化了。

在这里插入图片描述
一般是数据类型不匹配或值范围不正确导致的。

解决方法:把获得的高斯图像矩阵*255,把数据扩展为0-256。

参考:使用opencv中的方法进行图像保存时,出现保存的图像全黑的问题

2. 使用canny算子时报错

问题:异常:Microsoft C++ exception: cv::Exception at memory location xxxxxxxx

首先应排除下是否有读入图片,避免图片或文件放置的位置不正确、命名错误、格式等错误。

如果都不是的话,那就从代码看起。大部分情况下都是某个位置的参数、变量或者是函数调用等出错,这个没有固定的解决方案,只能自己设断点,查程序慢慢找。

本文中的问题在于传入的图片为CV_32F类型3通道,需要转换为CV_8U,通道可转可不转。

解决方案:主要使用Mat::convertTo()函数。

函数定义如下

void convertTo( OutputArray m, int rtype, double alpha=1, double beta=0 ) const;

参数说明:

  1. m – 目标矩阵。如果m在运算前没有合适的尺寸或类型,将被重新分配。
  2. rtype – 目标矩阵的类型。因为目标矩阵的通道数与源矩阵一样,所以rtype也可以看做是目标矩阵的位深度。如果rtype为负值,目标矩阵和源矩阵将使用同样的类型。
  3. alpha – 尺度变换因子(可选)。
  4. beta – 附加到尺度变换后的值上的偏移量(可选)。

实际的使用

// 高斯滤波的结果矩阵Gresult
Gresult.convertTo(edges, CV_8U, 255);       // 将输入图像从CV_32F转换为CV_8U

该解决办法涉及到Mat的数据类型参考

  1. 一般图像文件格式使用的是unsigned 8bits,对应的数据类型有:CV_8UC1、CV_8UC2,CV_8UC3,其中,CV_8UC3表示3通道8位的unsigned char型。
  2. float是32位,对应的CvMat数据结构类型是:CV_32FC1、CV_32FC2、CV_32FC3
  3. double是64位,对应的CvMat数据结构类型是:CV_64FC1,CV_64FC2,CV_64FC3,其中,CV_64FC3表示64位的3通道double型

想实现不同数据类型之间的转化,需要使用convertTo函数。

需要注意的是

  1. 公式里用到了exp函数,图片默认是unsigned char型,范围是0-255,公式中的0.5是0-1范围内的,所以需要将原图像转换为double型,将灰度值都转换为0-1之间,就可以利用该公式进行相应的运算。

  2. 处理完以后,图像还是CV_64FC3类型,如果直接保存,点开图像会发现,显示全黑,这是因为电脑默认打开图片是CV_8UC3类型的,所以还需要将CV_64FC3乘以255,再转换为CV_8UC3。

  3. Mat保存图片时,只能保存unsigned 8bits类型的格式,其他格式可以显示但不能imwrite

三通道与单通道的互转问题参考上面连接。

3. 文件操作

这不算是一个问题,只是正好用到,就一起写下来。

参考:
c++的文件操作输入输出流fstream

本文的使用方式

// 用于输出相机响应函数
 ofstream responseTxt;
 responseTxt.open("./pic/welding/responseDebevec.txt",ios::out);
 responseTxt << " 相机响应函数responseDebevec:" << endl << responseDebevec << endl;
 responseTxt.close();
GitHub 加速计划 / opencv31 / opencv
142
15
下载
OpenCV: 开源计算机视觉库
最近提交(Master分支:3 个月前 )
d9a139f9 Animated WebP Support #25608 related issues #24855 #22569 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake 1 天前
09030615 V4l default image size #25500 Added ability to set default image width and height for V4L capture. This is required for cameras that does not support 640x480 resolution because otherwise V4L capture cannot be opened and failed with "Pixel format of incoming image is unsupported by OpenCV" and then with "can't open camera by index" message. Because of the videoio architecture it is not possible to insert actions between CvCaptureCAM_V4L::CvCaptureCAM_V4L and CvCaptureCAM_V4L::open so the only way I found is to use environment variables to preselect the resolution. Related bug report is [#25499](https://github.com/opencv/opencv/issues/25499) Maybe (but not confirmed) this is also related to [#24551](https://github.com/opencv/opencv/issues/24551) This fix was made and verified in my local environment: capture board AVMATRIX VC42, Ubuntu 20, NVidia Jetson Orin. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [X] I agree to contribute to the project under Apache 2 License. - [X] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [X] The PR is proposed to the proper branch - [X] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake 1 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐