OpenCV数字图像处理实战一:去水印(C++)
1、简单版去水印
1.1 获取原图
// 1. 获取原图
Mat src = imread("E:\\img\\3.jpg");
if (src.empty())
{
cout << "No Image!" << endl;
system("pause");
return -1;
}
imshow("原图", src);
1.2 灰度化
// 2. 灰度化
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
imshow("灰度图", gray);
1.3 二值化
// 3. 图像二值化,筛选出白色区域部分
Mat binary;
// 在这里使用图像的平均值作为阈值T
Scalar T = mean(gray);
threshold(gray, binary, 220, 255, THRESH_BINARY);
imshow("二值化图", binary);
1.4 生成掩膜图
最关键在这一步,这部分的掩膜图像没生成好,就会导致生成的效果较差。因此,需要根据实际图片遍历图像元素并进行掩码。
// 4.提取图片下方的水印,制作掩模图像
Mat mask = Mat::zeros(src.size(), CV_8U);
int start_x = 0.8 * src.rows;
int end_x = src.rows;
int start_y = 0.8 * src.cols -40;
int end_y = src.cols;
//遍历图像像素,提取出水印部分像素,制作掩模图像
for (int i = start_x; i < end_x; i++)
{
uchar* data = binary.ptr<uchar>(i);
for (int j = start_y; j < end_y; j++)
{
//cout << binary.ptr<uchar>(i)[j]<<" ";
if (data[j] == 255)
{
mask.at<uchar>(i, j) = 255;
}
}
cout << endl;
}
imshow("掩膜图", mask);
1.5 膨胀
直接使用提取出的二值掩模进行图像修复得到的结果,可以看出效果不是很好。原因是,提取出来的掩模未能覆盖完全待修复像素。故我们需要将掩模图像进行膨胀操作,扩大掩模范围。
// 5.将掩模进行膨胀,使其能够覆盖图像更大区域
Mat kernel = getStructuringElement(MORPH_RECT, Size(6, 6));
dilate(mask, mask, kernel);
imshow("膨胀图", mask);
getStructuringElement(int shape, Size ksize, Point anchor)
需要输入两个参数:
一个是原始图像,
一个被称为结构化元素或核,它是用来决定操作的性质的
getStructuringElement源码介绍看附录
1.6 修复
这里使用opencv自带的图像修复函数,图像修复技术原理是利用已被破坏的边缘,即边缘的颜色和结构,繁殖和混合到损坏的图像中,已达到图像修补的目的。
// 6.使用inpaint进行图像修复
Mat result;
inpaint(src, mask, result, 8, INPAINT_NS);
imshow("image show", result);
void inpaint(
InputArray src,
InputArray inpaintMask,
OutputArray dst,
double inpaintRadius,
int flags );
第一个参数src,输入的单通道或三通道图像;
第二个参数inpaintMask,图像的掩码,单通道图像,大小跟原图像一致,inpaintMask图像上除了需要修复的部分之外其他部分的像素值全部为0;
第三个参数dst,输出的经过修复的图像;
第四个参数inpaintRadius,修复算法取的邻域半径,用于计算当前像素点的差值;
第五个参数flags,修复算法,有两种:INPAINT_NS 和I NPAINT_TELEA;
2、进阶版去水印
2.1 获取原图
// 1. 获取原图
Mat dst = imread("E:\\img\\4.jpg");
if (dst.empty())
{
cout << "No Image!" << endl;
system("pause");
return -1;
}
2.2 通过鼠标定位水印位置
void on_mouse(int event, int x, int y, int flags, void* userdata) {
Mat dst = (*(Mat*)userdata).clone();
Mat src(*(Mat*)userdata);
char temp[16];
//判断左键按下,记录起始点坐标
if (event == EVENT_LBUTTONDOWN) {
point1.x = x;
point1.y = y;
}
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {
//计算需画的矩形
//Rect rect(startP.x, startP.y, x - startP.x, y - startP.y);
//在图像上画出矩形
point2.x = x;
point2.y = y;
sprintf_s(temp, "(%d,%d)", x, y);//坐标
putText(dst, temp, point2, FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 0, 255));//实时显示鼠标移动的坐标
rectangle(dst, point1, Point(x, y), Scalar(250, 0, 0), 2, 0);
imshow("image show", dst);
}
//左键松开,在原图像画出矩形
else if (event == EVENT_LBUTTONUP) {
point2.x = x;
point2.y = y;
//Rect rect(point1.x, point1.y, x - point1.x, y - point1.y);
rectangle(dst, point1, point2, Scalar(0, 0, 250), 2, 0);
imshow("image show", dst);
}
}
(1)按住左键进行移动,此时会显示当前坐标信息以及当前选中的区域(用蓝色标出)。
(2)释放左键,表示已经选好要去水印的区域,此时用红色矩形框标出。
2.3 使用周围像素进行替换
选好区域后,在区域内进行像素替换,使用周围像素进行替换,偏移方向和距离可根据实际情况自定义。
//对选中区域进行像素替换,偏移3个像素,根据实际情况调节
for (int i = min(point1.y, point2.y); i < max(point2.y, point1.y); i++)
{
for (int j = min(point1.x, point2.x); j < max(point2.x, point1.x); j++)
{
src.at<Vec3b>(i, j)[0] = src.at<Vec3b>(i, j - 3)[0];
src.at<Vec3b>(i, j)[1] = src.at<Vec3b>(i, j - 3)[1];
src.at<Vec3b>(i, j)[2] = src.at<Vec3b>(i, j - 3)[2];
}
}
imshow("imagg", src);
2.4 对局部区域进行平滑处理
对替换好像素的区域进行平滑处理,去除噪声
//对选中区域周围进行平滑处理
Mat imageroi = src(Range(point1.y - 3, point2.y + 3), Range(point1.x - 3, point2.x + 3));
GaussianBlur(imageroi, imageroi, Size(15, 15), 0, 0);
2.5 将平滑结果粘贴到替换区域
使用copyTo将平滑后的区域粘贴到原图像上。
Mat img1 = (src).clone();
Mat imgdst = img1(Rect(point1.x - 3,point1.y - 3, point2.x - point1.x, point2.y - point1.y));
imageroi.copyTo(imgdst);
2.6 美颜(非必须)
对整体图像双边滤波(对人像有美颜效果)。
//对整体图像双边滤波(对人像有美颜效果)
bilateralFilter(img1, dst, 15, 30, 5);
附录
getStructuringElement源码介绍
cv::Mat cv::getStructuringElement(int shape, Size ksize, Point anchor)
{
int i, j;
int r = 0, c = 0;
double inv_r2 = 0;
CV_Assert( shape == MORPH_RECT || shape == MORPH_CROSS || shape == MORPH_ELLIPSE ); //目前支持三种形状的单元创建: 矩形, 十字形, 椭圆形;
anchor = normalizeAnchor(anchor, ksize); //当默认为-1,-1时, 计算anchor;
if( ksize == Size(1,1) ) //当给定大小为1,1时,表明是一个点, 可以用矩形来表示;
shape = MORPH_RECT;
if( shape == MORPH_ELLIPSE ) //椭圆;
{
r = ksize.height/2;
c = ksize.width/2;
inv_r2 = r ? 1./((double)r*r) : 0;
}
Mat elem(ksize, CV_8U);
for( i = 0; i < ksize.height; i++ ) //对每一行,计算0,1的范围;
{
uchar* ptr = elem.ptr(i);
int j1 = 0, j2 = 0;
if( shape == MORPH_RECT || (shape == MORPH_CROSS && i == anchor.y) ) //矩形,或十字y锚点时 j2为ksize.width;
j2 = ksize.width;
else if( shape == MORPH_CROSS )
j1 = anchor.x, j2 = j1 + 1;
else //椭圆;
{
int dy = i - r;
if( std::abs(dy) <= r )
{
int dx = saturate_cast<int>(c*std::sqrt((r*r - dy*dy)*inv_r2)); //计算得到x的偏移;
j1 = std::max( c - dx, 0 );
j2 = std::min( c + dx + 1, ksize.width );
}
}
for( j = 0; j < j1; j++ ) //从这三个for可以看出, (0,j1)之间为 0, (j1, j2)之间为1, (j2, ksize.width)之间为0;
ptr[j] = 0;
for( ; j < j2; j++ )
ptr[j] = 1;
for( ; j < ksize.width; j++ )
ptr[j] = 0;
}
return elem;
}