opencv 学习: 09 邻近像素处理,以高通滤波图片锐化为例
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 rowconst uchar *current = origin.ptr<const uchar>(j); // current rowconst uchar *next = origin.ptr<const uchar>(j + 1); // next rowuchar *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 0sharpen_v1(input_image, sharpen_image);#elsesharpen_v2(input_image, sharpen_image);
#endifconst 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);
}
