• OpenCV数字图像处理实战一:去水印(C++)


    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20221011151939877

    1.2 灰度化

       //   2. 灰度化
            Mat gray;
            cvtColor(src, gray, COLOR_BGR2GRAY);
            imshow("灰度图", gray);
    
    • 1
    • 2
    • 3
    • 4

    image-20221011151921669

    1.3 二值化

    //  3. 图像二值化,筛选出白色区域部分
            Mat binary;
            //  在这里使用图像的平均值作为阈值T
            Scalar T = mean(gray);
            threshold(gray, binary, 220, 255, THRESH_BINARY);
            imshow("二值化图", binary);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    image-20221011151907703

    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(i);
                for (int j = start_y; j < end_y; j++)
                {
                    //cout << binary.ptr(i)[j]<<" ";
                    if (data[j] == 255)
                    {
                        mask.at(i, j) = 255;
                    }
                    
                }
                cout << endl;
            }
            imshow("掩膜图", mask);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    image-20221011151848057

    1.5 膨胀

    直接使用提取出的二值掩模进行图像修复得到的结果,可以看出效果不是很好。原因是,提取出来的掩模未能覆盖完全待修复像素。故我们需要将掩模图像进行膨胀操作,扩大掩模范围。

    //  5.将掩模进行膨胀,使其能够覆盖图像更大区域
            Mat kernel = getStructuringElement(MORPH_RECT, Size(6, 6));
            dilate(mask, mask, kernel);
            imshow("膨胀图", mask);
    
    • 1
    • 2
    • 3
    • 4

    image-20221011152016720

    getStructuringElement(int shape, Size ksize, Point anchor)
    需要输入两个参数: 
    一个是原始图像, 
    一个被称为结构化元素或核,它是用来决定操作的性质的
    getStructuringElement源码介绍看附录
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.6 修复

    这里使用opencv自带的图像修复函数,图像修复技术原理是利用已被破坏的边缘,即边缘的颜色和结构,繁殖和混合到损坏的图像中,已达到图像修补的目的。

    //  6.使用inpaint进行图像修复
            Mat result;
            inpaint(src, mask, result, 8, INPAINT_NS);
            imshow("image show", result);
    
    • 1
    • 2
    • 3
    • 4

    image-20221011151815927

    void inpaint( 
        InputArray src, 
        InputArray inpaintMask,
        OutputArray dst, 
        double inpaintRadius, 
        int flags );
    第一个参数src,输入的单通道或三通道图像;
    第二个参数inpaintMask,图像的掩码,单通道图像,大小跟原图像一致,inpaintMask图像上除了需要修复的部分之外其他部分的像素值全部为0;
    第三个参数dst,输出的经过修复的图像;
    第四个参数inpaintRadius,修复算法取的邻域半径,用于计算当前像素点的差值;
    第五个参数flags,修复算法,有两种:INPAINT_NS 和I NPAINT_TELEA;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2、进阶版去水印

    2.1 获取原图

    //  1. 获取原图
        Mat dst = imread("E:\\img\\4.jpg");
        if (dst.empty())
        {
            cout << "No Image!" << endl;
            system("pause");
            return -1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20221011164923130

    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    (1)按住左键进行移动,此时会显示当前坐标信息以及当前选中的区域(用蓝色标出)。

    image-20221011164959723

    (2)释放左键,表示已经选好要去水印的区域,此时用红色矩形框标出。

    image-20221011165029112

    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(i, j)[0] = src.at(i, j - 3)[0];
            src.at(i, j)[1] = src.at(i, j - 3)[1];
            src.at(i, j)[2] = src.at(i, j - 3)[2];
    
    
        }
    }
    
    imshow("imagg", src);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20221011165048270

    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);
    
    • 1
    • 2
    • 3

    image-20221011165110215

    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);
    
    • 1
    • 2
    • 3

    image-20221011165123051

    2.6 美颜(非必须)

    对整体图像双边滤波(对人像有美颜效果)。

     //对整体图像双边滤波(对人像有美颜效果)
    bilateralFilter(img1, dst, 15, 30, 5);
    
    • 1
    • 2

    image-20221011165141457

    附录

    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(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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
  • 相关阅读:
    flowable 会签例子
    贪心算法解决雷达站建站问题
    【数据结构初阶】四、线性表里的链表(带头+双向+循环 链表)
    贝叶斯网络预测相关问题
    网络安全笔记-WebShell与文件上传
    网络协议:应用层
    力扣刷题:整数反转、 字符串转换整数 (atoi)、回文数
    计算机毕业设计之java+springboot基于vue的广场舞团社团网站-社团管理系统
    dvadmin-打包发布-nginx-静态服务器配置-防火墙设置
    CSS(七)用户样式+vertical-align属性+文字省略号显示
  • 原文地址:https://blog.csdn.net/qq_43784519/article/details/127267579