摸鱼摸鱼,今天试试用简单的频域隐写水印~
其实隐写水印的方法也有不少了,这里是其中一个简单的方法,使用的是离散傅立叶变换,相比起小波变换的版本,这里的鲁棒性没有那么强,但也还是挺好玩的,下次有时间可以试试看小波变换的方法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()); } } }