1.说明
在图像处理中,经常需要根据相邻像素值来计算当前像素值。
比如下面表格为例,计算 (1,1) 位置的这个像素值,需要参考它周边相邻这一圈的,不同行或列的像素的值。
| 0,0 | 0,1 | 0,2 |
|---|---|---|
| 1,0 | 1,1 | 1,2 |
| 2,0 | 2,1 | 2,2 |
具体的,以图像锐化为例子,进行说明;其中用到了 “拉普拉斯算子”,这里先不深入原理细节,在后面的部分补充。
图像锐化:
就是图像中边缘对比度增加,使得交界变得明显,锐利,细节增加。
适度锐化,可以消除一些模糊,增强一些边界信息。
过度锐化,会使得细节信息变得过度突出,反而引入噪声和伪影。
拉普拉斯算子:
[ 0, -1, 0]
[-1, 5, -1]
[ 0, -1, 0]
简化成式子就是:
当前像素新值 = 当前像素值 * 5 - 上下左右四个相邻像素值
另外,因为图像边缘的行和列没有完整的四个相邻的像素,所以不做处理。
2.实现代码
#include <iostream>
#include <opencv2/opencv.hpp>
#include <chrono>
//at方式实现,效率低,但是直观一些
//也限制为彩色图片,主要为了说明计算过程
void sharpen_v1(const cv::Mat& origin, cv::Mat& result)
{
//分配结果图片空间
//当为同源图片时,会直接返回,引用同一张图片数据,修改原图
result.create(origin.size(), origin.type());
//遍历行,跳过第一行,最后一行
for (size_t i = 1; i < (origin.rows-1); i++)
{
//遍历列,跳过第一列和最后一列
for (size_t j = 1; j < (origin.cols-1); j++)
{
//当前像素
cv::Vec3b* pixel = result.ptr<cv::Vec3b>(i, j);
#if 0 //有问题版本
//原图中像素,及其相邻像素值
//不能这么直接用Vec3b,因为cv::Vec3b 是 3个uchar 组成的,
//5*pixel_center 会溢出,截取溢出剩余的值进行计算
cv::Vec3b pixel_center = origin.at<cv::Vec3b>(i, j);
cv::Vec3b pixel_up = origin.at<cv::Vec3b>(i-1, j);
cv::Vec3b pixel_down = origin.at<cv::Vec3b>(i+1, j);
cv::Vec3b pixel_left = origin.at<cv::Vec3b>(i, j-1);
cv::Vec3b pixel_right = origin.at<cv::Vec3b>(i, j+1);
#else //正确版本
//原图中像素,及其相邻像素值
//cv::Vec3s 是 3个short 组成的,不会溢出,计算完成再转换为uchar, 当然如果算子过大,也可能会溢出
cv::Vec3s pixel_center = origin.at<cv::Vec3b>(i, j);
cv::Vec3s pixel_up = origin.at<cv::Vec3b>(i-1, j);
cv::Vec3s pixel_down = origin.at<cv::Vec3b>(i+1, j);
cv::Vec3s pixel_left = origin.at<cv::Vec3b>(i, j-1);
cv::Vec3s pixel_right = origin.at<cv::Vec3b>(i, j+1);
#endif
//pixel_center 等为,cv::Vec3s 时将结果做个隐式转换
*pixel = 5 * pixel_center - pixel_up - pixel_down - pixel_left - pixel_right;
}
}
}
//指针方式实现,效率高
void sharpen_v2(const cv::Mat& origin, cv::Mat& result)
{
//分配结果图片空间
//当为同源图片时,会直接返回,引用同一张图片数据,修改原图
result.create(origin.size(), origin.type());
int nchannels = origin.channels();
//遍历行,跳过第一行,最后一行
for (int j = 1; j < origin.rows - 1; j++)
{
const uchar *previous = origin.ptr<const uchar>(j - 1); // previous row
const uchar *current = origin.ptr<const uchar>(j); // current row
const uchar *next = origin.ptr<const uchar>(j + 1); // next row
uchar *output = result.ptr<uchar>(j); // output row
//遍历列,跳过第一列和最后一列
for (int i = nchannels; i < (origin.cols - 1) * nchannels; i++)
{
//current[i - nchannels]: 当前像素左侧像素对应通道值
//current[i + nchannels]:当前像素右侧像素对应通道值
//previous[i]:当前像素上一行像素对应通道值
//next[i]:当前像素下一行像素对应通道值
*output = cv::saturate_cast<uchar>( 5 * current[i] - current[i - nchannels] -
current[i + nchannels] - previous[i] - next[i]);
output++;
}
}
}
int main(int argc, char *argv[])
{
// 检查命令行参数
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " <input_image> <output_image>" << std::endl;
return -1;
}
// 读取输入图像和logo图像
cv::Mat input_image = cv::imread(argv[1]);
// 检查输入图像和logo图像是否成功读取
if (input_image.empty())
{
std::cerr << "Error: Could not open or find input image" << std::endl;
}
cv::namedWindow("input_image", cv::WINDOW_NORMAL);
cv::imshow("input_image", input_image);
cv::waitKey(0);
cv::Mat sharpen_image;
const int64 start = cv::getTickCount();
#if 0
sharpen_v1(input_image, sharpen_image);
#else
sharpen_v2(input_image, sharpen_image);
#endif
const int64 end = cv::getTickCount();
std::cout << "sharpen used time: " << (end - start) / cv::getTickFrequency() << "s" << std::endl;
cv::imwrite(argv[2], sharpen_image);
cv::namedWindow("sharpen_image", cv::WINDOW_NORMAL);
cv::imshow("sharpen_image", sharpen_image);
cv::waitKey(0);
return 0;
}
3.执行效果
如下图:锐化后,图片的细节会增多。视觉上感受就是变清晰了。有些纹理细节也会便突出。
将GIF拖拽到单独页面,再放大查看。会比较明显,特别是中部的人物衣物纹理等。

4. cv::filter2D 函数
由于 滤波 (filtering)是个很常见的操作,所以 opencv 提供了一个对应的 cv::filter2D 函数。
需要先定义一个算子,然后将目标图片和算子传入,获得处理结果。重新实现的方法如下:
void sharpen_v3(const cv::Mat& origin, cv::Mat& result)
{
//分配结果图片空间
//当为同源图片时,会直接返回,引用同一张图片数据,修改原图
result.create(origin.size(), origin.type());
//定义算子
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
0, -1, 0,
-1, 5, -1,
0, -1, 0);
cv::filter2D(origin, result, origin.depth(), kernel);
}
2129

被折叠的 条评论
为什么被折叠?



