A Simple Approach to Add Invisible Watermark with OpenCV

摸鱼摸鱼,今天试试用简单的频域隐写水印~

其实隐写水印的方法也有不少了,这里是其中一个简单的方法,使用的是离散傅立叶变换,相比起小波变换的版本,这里的鲁棒性没有那么强,但也还是挺好玩的,下次有时间可以试试看小波变换的方法233333

在频域增加水印的好处是肉眼不易看见,而且对于一般的裁剪、拉伸、涂抹有较强的抵抗性~比如下面的图像就加上隐写的水印,但是与左侧的原图几乎没有视觉上的差异。

在将两张图转换到频域上之后,则一眼能看到右侧图上的水印~

裁剪之后的效果~

重新调整大小后~

修改图片w

当然了,后文代码里的方法比较简单,多做一些改动再拿 JEPG 压缩一下就没了(因为 JPEG 压缩也是把高频的部分给去掉,正好对应此处隐写的方法,隐写的信息被当作噪音给过滤掉了~)

代码其实真的挺简单的,下次用小波变换玩玩好了2333333

tachikoma.hpp 的部分如下~

#ifndef __TACHIKOMA_HPP
#define __TACHIKOMA_HPP

#include <vector>
#include <string>
#include <opencv2/opencv.hpp>

namespace tachikoma {
    
/**
 为离散傅立叶变换优化图像
 
 @param image 原始图像
 @return 为离散傅立叶变换优化后的图像
 */
cv::Mat getOptimalImage(const cv::Mat& image);

/**
 计算频域图像
 
 @param padded 图像大小已经优化后的 cv::Mat
 @return 离散傅立叶变换后的频域图像
 */
cv::Mat getComplexImage(cv::Mat& padded);

/**
 逆离散傅立叶变换
 
 @param complexImage 频域图像
 @return 逆离散傅立叶变换后的图像
 */
cv::Mat invDFT(const cv::Mat& complexImage);

/**
 计算 Magnitude
 
 @param complexImage 频域图像
 @return 对应的 Magnitude
 */
cv::Mat getOptimizedMagnitude(const cv::Mat& complexImage);

/**
 添加隐水印
 
 @param image 要添加隐水印的图像
 @param watermark 水印
 @param point 水印位置
 @param font_size 水印字号
 @param color 水印颜色
 @return 添加完隐水印的图像
 */
cv::Mat addWatermark(const cv::Mat& image, std::string watermark, cv::Point point, double font_size, cv::Scalar color) {
    std::vector<cv::Mat> allPlanes;
    cv::Mat optimalImage = getOptimalImage(image);
    
    // 分离原图像的通道
    cv::split(optimalImage, allPlanes);
    cv::Mat padded = cv::Mat();
    if (allPlanes.size() > 1) {
        // 多通道图像则提取其 B channel 即可
        padded = allPlanes[0];
    } else {
        padded = optimalImage;
    }
    
    cv::Mat complexImage = getComplexImage(padded);
    
    // 在频谱图上中心对称地增加水印文字
    cv::putText(complexImage, watermark, point, cv::FONT_HERSHEY_DUPLEX, font_size, color, 4);
    cv::flip(complexImage, complexImage, -1);
    cv::putText(complexImage, watermark, point, cv::FONT_HERSHEY_DUPLEX, font_size, color, 4);
    cv::flip(complexImage, complexImage, -1);
    
    cv::Mat restoredImage = invDFT(complexImage);
    // 合并多通道
    allPlanes.erase(allPlanes.begin());
    allPlanes.insert(allPlanes.begin(), restoredImage);
    cv::Mat result;
    cv::merge(allPlanes, result);
    int y = result.rows - image.rows;
    int x = result.cols - image.cols;
    result = result(cv::Rect(x, y, result.cols - x, result.rows - y));
    
    return result;
}

/**
 计算给定图像的频域图
 
 @param image 需要计算频域图的图像
 @return 给定图像的频域图
 */
cv::Mat getWatermark(const cv::Mat& image) {
    std::vector<cv::Mat> allPlanes;
    cv::Mat optimalImage = getOptimalImage(image);
    
    // 分离原图像的通道
    cv::split(optimalImage, allPlanes);
    cv::Mat padded = cv::Mat();
    if (allPlanes.size() > 1) {
        // 多通道图像则提取其第一个 channel 即可
        padded = allPlanes[0];
    } else {
        padded = optimalImage;
    }
    
    cv::Mat complexImage = getComplexImage(padded);
    cv::Mat magnitude = getOptimizedMagnitude(complexImage);
    return magnitude;
}

/**
 计算 Magnitude
 
 @param complexImage 频域图像
 @return 对应的 Magnitude
 */
cv::Mat getOptimizedMagnitude(const cv::Mat& complexImage) {
    std::vector<cv::Mat> _newPlanes;
    cv::Mat magnitude = cv::Mat();
    cv::split(complexImage, _newPlanes);
    cv::magnitude(_newPlanes[0], _newPlanes[1], magnitude);
    cv::add(cv::Mat::ones(magnitude.size(), CV_32F), magnitude, magnitude);
    cv::log(magnitude, magnitude);
    
    // shift DFT
    magnitude = magnitude(cv::Rect(0, 0, magnitude.cols & (-2), magnitude.rows & (-2)));
    
    // rearrange the quadrants of Fourier image
    // so that the origin is at the image center
    int cx = magnitude.cols / 2;
    int cy = magnitude.rows / 2;
    
    cv::Mat q0 = cv::Mat(magnitude, cv::Rect(0, 0, cx, cy));
    cv::Mat q1 = cv::Mat(magnitude, cv::Rect(cx, 0, cx, cy));
    cv::Mat q2 = cv::Mat(magnitude, cv::Rect(0, cy, cx, cy));
    cv::Mat q3 = cv::Mat(magnitude, cv::Rect(cx, cy, cx, cy));
    
    // exchange 1 and 4 quadrants
    cv::Mat tmp =  cv::Mat();
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);
    
    // exchange 2 and 3 quadrants
    q1.copyTo(tmp);
    q2.copyTo(q1);
    tmp.copyTo(q2);
    
    magnitude.convertTo(magnitude, CV_8UC1);
    cv::normalize(magnitude, magnitude, 0, 255, cv::NORM_MINMAX, CV_8UC1);
    return magnitude;
}

/**
 为离散傅立叶变换优化图像
 
 @param image 原始图像
 @return 为离散傅立叶变换优化后的图像
 */
cv::Mat getOptimalImage(const cv::Mat& image) {
    cv::Mat optimalImage = cv::Mat();
    // get the optimal rows size for dft
    int optimalRows = cv::getOptimalDFTSize(image.rows);
    // get the optimal cols size for dft
    int optimalCols = cv::getOptimalDFTSize(image.cols);
    // apply the optimal cols and rows size to the image
    cv::copyMakeBorder(image, optimalImage, optimalRows - image.rows, 0, optimalCols - image.cols, cv::BORDER_CONSTANT, 0);
    
    return optimalImage;
}

/**
 计算频域图像
 
 @param padded 图像大小已经优化后的 cv::Mat
 @return 离散傅立叶变换后的频域图像
 */
cv::Mat getComplexImage(cv::Mat& padded) {
    cv::Mat complexImage;
    std::vector<cv::Mat> planes;
    
    // prepare the image planes to obtain the complex image
    padded.convertTo(padded, CV_32F);
    planes.push_back(padded);
    planes.push_back(cv::Mat::zeros(padded.size(), CV_32F));
    cv::merge(planes, complexImage);
    // 离散傅立叶变换
    cv::dft(complexImage, complexImage);
    
    return complexImage;
}

/**
 逆离散傅立叶变换
 
 @param complexImage 频域图像
 @return 逆离散傅立叶变换后的图像
 */
cv::Mat invDFT(const cv::Mat& complexImage) {
    cv::Mat invDFT;
    cv::idft(complexImage, invDFT, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT, 0);
    cv::Mat restoredImage;
    invDFT.convertTo(restoredImage, CV_8U);
    return restoredImage;
}
    
};

#endif /* __TACHIKOMA_HPP */

main.cpp 则是~

#include <getopt.h>
#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
#include "tachikoma.hpp"

void print_help() {
    fprintf(stdout, "Usage:  -m,--mode [add|get]\n");
    fprintf(stdout, "        -i,--input Input image\n");
    fprintf(stdout, "        -w,--watermark \"Watermark\"\n");
    fprintf(stdout, "        -o,--output Output image\n");
    fprintf(stdout, "        -h,--help Print this help\n");
}

void parsearg(int argc, char * argv[],
              int& mode,
              std::string& input,
              std::string& output,
              std::string& watermark) {
    int c;
    
    while (1) {
        static struct option long_options[] = {
            {"help",      no_argument,       0, 'h'},
            {"mode",      required_argument, 0, 'm'},
            {"input",     required_argument, 0, 'i'},
            {"output",    required_argument, 0, 'o'},
            {"watermark", optional_argument, 0, 'w'},
            {0, 0, 0, 0}
        };
        /* getopt_long stores the option index here. */
        int option_index = 0;
        
        c = getopt_long (argc, argv, "hm:i:o:w:", long_options, &option_index);
        
        /* Detect the end of the options. */
        if (c == -1)
            break;
        
        switch (c) {
            case 'h': {
                print_help();
                break;
            }
            case 'm': {
                if (strcmp(optarg, "add") == 0) {
                    mode = 1;
                } else if (strcmp(optarg, "get") == 0) {
                    mode = 2;
                } else {
                    print_help();
                }
                break;
            }
            case 'i': {
                input = optarg;
                break;
            }
            case 'o': {
                output = optarg;
                break;
            }
            case 'w': {
                watermark = optarg;
                break;
            }
            default: {
                print_help();
                break;
            }
        }
    }
}

int main(int argc, const char * argv[]) {
    int mode = 0;
    std::string input, output, watermark;
    parsearg(argc, (char **)argv, mode, input, output, watermark);
    
    if (input.length() == 0 || output.length() == 0 || mode == 0) {
        print_help();
        exit(2);
    }
    
    cv::Mat image = cv::imread(input);
    if (image.empty()) {
        fprintf(stderr, "[ERROR] Cannot open image at %s\n", input.c_str());
        exit(1);
    }
    
    if (mode == 1) {
        if (watermark.length() == 0) {
            fprintf(stdout, "[WARN] Specified `add` mode without watermark string\n");
        }
        cv::Mat marked = tachikoma::addWatermark(image, watermark, cv::Point(100, 100), 1, CV_RGB(0, 255, 255));
        if (!cv::imwrite(output, marked)) {
            fprintf(stderr, "[ERROR] Cannot write output at: %s\n", output.c_str());
        }
    } else if (mode == 2) {
        cv::Mat watermark = tachikoma::getWatermark(image);
        if (!cv::imwrite(output, watermark)) {
            fprintf(stderr, "[ERROR] Cannot write output at: %s\n", output.c_str());
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

15 + 1 =