核心概念:从空间域到频率域的映射
在数字图像中,空间域(Spatial Domain)是指像素在二维坐标系上的亮度排列。而 DCT(Discrete Cosine Transform) 是一种正交线性变换,它将图像块的颜色分量分解为一系列具有不同频率的余弦波组合。
频率分量的物理意义
当一个 $8 \times 8$ 的像素方阵经过 DCT 变换后,会生成一个 64 阶的系数矩阵。
- 低频(Low Frequency):位于矩阵左上角,代表图像的基色和平均亮度(DC 分量)。
- 中频(Mid Frequency):位于矩阵中部,代表图像的轮廓特征与主要纹理。
- 高频(High Frequency):位于矩阵右下角,代表图像的边缘锐度、噪点和细微纹理。
能量转换的目的
将空间信号转换为频率分布,是为了实现能量集中。在大多数自然图像中,绝大部分能量(信息量)集中在低频部分,而水印算法的目标是找到一个既能抵抗压缩干扰、又不引起视觉察觉的频段,即“中频带”。
算法执行全流程
图像分块与色彩通道选择
算法首先将图像划分为不重叠的 $8 \times 8$ 像素块。通常选择 YCbCr 色彩空间的 Y(亮度)分量,或者 RGB 空间中的蓝色通道进行操作。这是因为人类视觉系统(HVS)对蓝色分量的微小变化敏感度较低。
正向 DCT 变换
利用分离变换(Separable Transform)特性,对每个像素块执行数学运算。此时,原本在空间坐标上分布的颜色强度被转化为 64 个频率系数。
水印信号的编码与嵌入
这是算法的逻辑核心。我们采用差分能量修改法,通过调整两个特定中频位置系数(例如 $DCT{4,3}$ 和 $DCT{3,4}$)的相对大小来携带二进制信息(0 或 1):
- 嵌入逻辑:
- 若嵌入 $Bit = 1$:通过数学补偿,使 $DCT{4,3}$ 显著大于 $DCT{3,4}$。
- 若嵌入 $Bit = 0$:通过数学补偿,使 $DCT{3,4}$ 显著大于 $DCT{4,3}$。
- 强度控制 (Strength):引入一个步长参数 $S$。修改后的系数差异需满足 $|A – B| > S$。$S$ 越大,水印在面对 JPEG 压缩、缩放等攻击时的存活率越高,但会导致图像产生块效应(Blocky artifacts)。
逆向 DCT 变换
修改完成后,执行 IDCT 运算。这一步将频率系数重新组合成空间域的像素值。由于修改仅发生在中频段,还原后的像素值与原像素值的偏差极小(通常在 $\pm 1$ 到 $\pm 3$ 之间),从而实现了视觉上的隐形。而实现了视觉上的隐形。
鲁棒性与稳定性保障机制
盲提取协议
该算法属于“盲水印”,即提取端无需原始图像参考。提取器仅需对目标图像再次进行 $8 \times 8$ 分块 DCT,直接观测预定坐标系数的差值方向。若 $DCT{4,3} > DCT{3,4}$,则判定为 1,反之为 0。
循环冗余嵌入
为了对抗图像剪裁(Cropping)攻击,算法将水印字符串(Payload)在整张图像的所有分块中进行循环嵌入。只要提取出的第一个字节符合预设的同步特征码(Sync Marker),即可确认信号起始位置。这意味着只要图像保留了足够的局部完整性,水印即可恢复。
抗压缩原理
JPEG 压缩通过量化表抹除高频系数以节省空间。DCT 水印将信号刻录在保留较完整的中频系数中,使其在经历剧烈的数据舍弃(重采样、有损压缩)后,系数间的相对比例关系依然能够维持稳定,从而确保了信息的持久性。
核心逻辑实现
原始算法
为了深入理解 DCT(离散余弦变换)水印算法,我们可以通过 C 实现一个核心演示版本。该算法直接实现 2D-DCT 的数学定义式:
$$F(u, v) = \frac{1}{4} C(u) C(v) \sum{x=0}^{7} \sum{y=0}^{7} f(x, y) \cos \left[ \frac{(2x+1)u\pi}{16} \right] \cos \left[ \frac{(2y+1)v\pi}{16} \right]$$
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/**
* 原始 2D-DCT 变换(标准四层循环实现)
* @param f 输入的 8x8 空间域像素矩阵
* @param F 输出的 8x8 频率域系数矩阵
*/
void raw_dct_2d(double f[8][8], double F[8][8]) {
for (int u = 0; u < 8; u++) {
for (int v = 0; v < 8; v++) {
double sum = 0.0;
// 核心:空间域坐标 (x, y) 的全量双重累加
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
sum += f[x][y] * cos((2 * x + 1) * u * M_PI / 16.0) * cos((2 * y + 1) * v * M_PI / 16.0);
}
}
// 计算正交归一化系数 alpha
double au = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
double av = (v == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
F[u][v] = au * av * sum;
}
}
}
/**
* 原始 2D-IDCT 逆变换(标准四层循环实现)
* 用于从频率分布 F 还原回空间信号 f
*/
void raw_idct_2d(double F[8][8], double f[8][8]) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
double sum = 0.0;
for (int u = 0; u < 8; u++) {
for (int v = 0; v < 8; v++) {
double au = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
double av = (v == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
sum += au * av * F[u][v] * cos((2 * x + 1) * u * M_PI / 16.0) * cos((2 * y + 1) * v * M_PI / 16.0);
}
}
f[x][y] = sum;
}
}
}
在原始算法中,为了得到 64 个 $F(u, v)$ 系数,每一个系数都需要进行 $8 \times 8 = 64$ 次乘法累加。总计算量为 $64 \times 64 = 4096$ 次核心运算。其复杂度为 $O(N^4)$(其中 $N=8$)。对于一张 $1080p$ 的图片,大约有 $32,400$ 个 $8 \times 8$ 块,这意味着总计算量接近 1.3 亿次浮点运算。
优化方案
经过优化后的完整代码参见:DCT_Watermark
查表法
cos 函数的计算非常昂贵。而在 $8 \times 8$ 的 DCT 中,cos 的输入值只有 64 种固定的组合。预先计算好这 64 个值,存入一个静态数组,可是计算速度增加数倍。优化后的代码如下所示:
/* 全局静态查找表:存储预计算的余弦系数,消除重复的三角函数运算开销 */
static double COS_TABLE[8][8];
static int table_initialized = 0;
/**
* init_tables: 预计算 DCT 系数表
* 依据 DCT-II 标准公式,提前计算 cos((2j+1)iπ / 16) 并缓存
*/
void init_tables() {
int i, j;
if (table_initialized) return;
for (i = 0; i < 8; i++) {
for (j = 0; j < 8; j++) {
COS_TABLE[i][j] = cos((2 * j + 1) * i * M_PI / 16.0);
}
}
table_initialized = 1;
}
可分离变换
二维 DCT 可以分解为两次一维 DCT。先对每一行做 1D-DCT,再对结果的每一列做 1D-DCT。通过 dct_1d 分两次处理行和列,可以将复杂度从 $O(N^4)$ 降至 $O(N^3)$ 。优化后的代码如下所示:
/**
* fdct_1d: 一维正向离散余弦变换 (Forward DCT)
* 将空间域信号转换为频率域能量分布
*/
void fdct_1d(double in[8], double out[8]) {
int i, j;
for (i = 0; i < 8; i++) {
double sum = 0;
for (j = 0; j < 8; j++) {
sum += in[j] * COS_TABLE[i][j];
}
// 应用正交化系数 Alpha
double alpha = (i == 0) ? 0.35355339 : 0.5; // sqrt(1/8) 与 sqrt(2/8)
out[i] = alpha * sum;
}
}
/**
* idct_1d: 一维逆向离散余弦变换 (Inverse DCT)
* 将频率域系数还原回空间域像素值
*/
void idct_1d(double in[8], double out[8]) {
int i, j;
for (j = 0; j < 8; j++) {
double sum = 0;
for (i = 0; i < 8; i++) {
double alpha = (i == 0) ? 0.35355339 : 0.5;
sum += alpha * in[i] * COS_TABLE[i][j];
}
out[j] = sum;
}
}
/**
* fdct_8x8_fast: 二维快速 DCT 实现
* 利用 DCT 的可分离特性 (Separability),通过两次一维变换完成二维矩阵运算
* 复杂度由 O(N^4) 降至 O(N^3)
*/
void fdct_8x8_fast(double in[8][8], double out[8][8]) {
double temp[8][8];
int i, j;
// 步骤1:行变换
for (i = 0; i < 8; i++) fdct_1d(in[i], temp[i]);
// 步骤2:列变换
for (j = 0; j < 8; j++) {
double col_in[8], col_out[8];
for (i = 0; i < 8; i++) col_in[i] = temp[i][j];
fdct_1d(col_in, col_out);
for (i = 0; i < 8; i++) out[i][j] = col_out[i];
}
}
强度平衡
强度值(Strength)是一个平衡杆。28.0 是一个平衡点。数值越大,抗压缩越强,但图片可能出现色块(块效应)。
double strength = 28.0;
同步特征
在数据流开头加入特定的位模式(如 0xAA),可以帮助提取器快速定位起始位置并过滤无水印图片。
#define SYNC_MARKER 0xAA