Spring Boot 实现图片盲水印:基于 DCT 频域算法的隐形数字水印
盲水印区别于肉眼可见的文字水印,嵌入后肉眼几乎看不出差异,可通过算法提取水印信息,常用于版权溯源、图片溯源场景。
主流两种方案:
| 方案 | 原理 | 优势 | 劣势 |
|---|---|---|---|
| 频域水印(DCT) | 修改图片像素频率数据嵌入信息 | 压缩、轻微裁剪后仍可提取,稳定性最佳 | 手写 DCT 数学计算有性能损耗 |
| 空域 LSB | 修改像素二进制末尾比特 | 肉眼无感,实现简单 | 图片压缩后水印极易丢失 |
下面基于 Java + DCT 频域算法 完整实现 Spring Boot 盲水印嵌入与提取,依赖原生图片处理工具 javax.imageio + 数学运算,无需额外重型 SDK。
一、核心原理
DCT(离散余弦变换)盲水印核心逻辑:
#mermaid-svg-TNWleozJHIABBVbA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TNWleozJHIABBVbA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TNWleozJHIABBVbA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TNWleozJHIABBVbA .error-icon{fill:#552222;}#mermaid-svg-TNWleozJHIABBVbA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TNWleozJHIABBVbA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TNWleozJHIABBVbA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TNWleozJHIABBVbA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TNWleozJHIABBVbA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TNWleozJHIABBVbA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TNWleozJHIABBVbA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TNWleozJHIABBVbA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TNWleozJHIABBVbA .marker.cross{stroke:#333333;}#mermaid-svg-TNWleozJHIABBVbA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TNWleozJHIABBVbA p{margin:0;}#mermaid-svg-TNWleozJHIABBVbA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TNWleozJHIABBVbA .cluster-label text{fill:#333;}#mermaid-svg-TNWleozJHIABBVbA .cluster-label span{color:#333;}#mermaid-svg-TNWleozJHIABBVbA .cluster-label span p{background-color:transparent;}#mermaid-svg-TNWleozJHIABBVbA .label text,#mermaid-svg-TNWleozJHIABBVbA span{fill:#333;color:#333;}#mermaid-svg-TNWleozJHIABBVbA .node rect,#mermaid-svg-TNWleozJHIABBVbA .node circle,#mermaid-svg-TNWleozJHIABBVbA .node ellipse,#mermaid-svg-TNWleozJHIABBVbA .node polygon,#mermaid-svg-TNWleozJHIABBVbA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TNWleozJHIABBVbA .rough-node .label text,#mermaid-svg-TNWleozJHIABBVbA .node .label text,#mermaid-svg-TNWleozJHIABBVbA .image-shape .label,#mermaid-svg-TNWleozJHIABBVbA .icon-shape .label{text-anchor:middle;}#mermaid-svg-TNWleozJHIABBVbA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TNWleozJHIABBVbA .rough-node .label,#mermaid-svg-TNWleozJHIABBVbA .node .label,#mermaid-svg-TNWleozJHIABBVbA .image-shape .label,#mermaid-svg-TNWleozJHIABBVbA .icon-shape .label{text-align:center;}#mermaid-svg-TNWleozJHIABBVbA .node.clickable{cursor:pointer;}#mermaid-svg-TNWleozJHIABBVbA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TNWleozJHIABBVbA .arrowheadPath{fill:#333333;}#mermaid-svg-TNWleozJHIABBVbA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TNWleozJHIABBVbA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TNWleozJHIABBVbA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TNWleozJHIABBVbA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TNWleozJHIABBVbA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TNWleozJHIABBVbA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TNWleozJHIABBVbA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TNWleozJHIABBVbA .cluster text{fill:#333;}#mermaid-svg-TNWleozJHIABBVbA .cluster span{color:#333;}#mermaid-svg-TNWleozJHIABBVbA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TNWleozJHIABBVbA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TNWleozJHIABBVbA rect.text{fill:none;stroke-width:0;}#mermaid-svg-TNWleozJHIABBVbA .icon-shape,#mermaid-svg-TNWleozJHIABBVbA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TNWleozJHIABBVbA .icon-shape p,#mermaid-svg-TNWleozJHIABBVbA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TNWleozJHIABBVbA .icon-shape .label rect,#mermaid-svg-TNWleozJHIABBVbA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TNWleozJHIABBVbA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TNWleozJHIABBVbA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TNWleozJHIABBVbA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 频率系数含义
低频:画面主体
中频:纹理细节
高频:噪点
彩色图片
转为灰度图像
划分 8×8 像素小块
DCT 变换:空间像素 → 频率系数
在中频系数嵌入水印比特
逆 DCT 还原像素块
生成带盲水印图片
关键设计:
- 将彩色图片转为灰度图像,划分成无数 8×8 像素小块
- 对每个像素块做 DCT 变换,把空间像素转为频率系数
- 在中频系数嵌入水印比特(修改系数大小):肉眼无法察觉,兼顾隐蔽性;压缩、轻微裁剪后依旧可以提取水印
- 提取时重新 DCT 变换,读取中频系数差值,还原水印文字
二、Maven 依赖
xml
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>3.15.2</version>
</dependency>
三、核心 DCT 盲水印工具类
包含:图片灰度转换、DCT 正/逆变换、水印比特嵌入、水印提取、文字↔二进制互转。
java
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
* DCT 频域盲水印工具类
* 支持:嵌入文本水印、提取水印文本
*/
public class DctBlindWatermarkUtil {
// 8*8 分块标准尺寸,DCT 固定大小
private static final int BLOCK_SIZE = 8;
// 水印强度:越大水印越牢固,痕迹越明显;推荐区间 8 ~ 16
private static final int STRENGTH = 12;
// 水印最大比特长度(限制水印文字长度)
private static final int MAX_BIT_LENGTH = 1024;
//==================== 对外入口:嵌入水印 ====================
/**
* 图片嵌入文本盲水印
* @param inputStream 原图输入流
* @param outputPath 加水印图片存储路径
* @param watermarkText 水印内容(版权、用户ID、溯源信息)
*/
public static void embed(InputStream inputStream, String outputPath, String watermarkText) throws Exception {
// 读取原图
BufferedImage originImg = ImageIO.read(inputStream);
int width = originImg.getWidth();
int height = originImg.getHeight();
// 图片转为灰度像素矩阵
double[][] grayMatrix = imgToGrayMatrix(originImg);
// 水印文字转为二进制比特数组
boolean[] waterBits = textToBits(watermarkText);
int bitCursor = 0;
// 遍历所有 8*8 像素块
for (int y = 0; y <= height - BLOCK_SIZE && bitCursor < waterBits.length; y += BLOCK_SIZE) {
for (int x = 0; x <= width - BLOCK_SIZE && bitCursor < waterBits.length; x += BLOCK_SIZE) {
// 截取 8*8 像素块
double[][] block = getBlock(grayMatrix, x, y);
// DCT 正变换:空域转频域
double[][] dctBlock = dctForward(block);
// 在中频位置嵌入水印比特
embedSingleBit(dctBlock, waterBits[bitCursor]);
// DCT 逆变换:频域转回空域像素
double[][] idctBlock = dctInverse(dctBlock);
// 将处理后的像素块写回矩阵
setBlock(grayMatrix, x, y, idctBlock);
bitCursor++;
}
}
// 灰度矩阵还原成图片
BufferedImage watermarkImg = grayMatrixToImg(grayMatrix, width, height);
// 写入文件
ImageIO.write(watermarkImg, "jpg", new File(outputPath));
}
//==================== 对外入口:提取水印 ====================
/**
* 从带水印图片提取水印文字
* @param inputStream 带盲水印的图片流
* @return 水印文本
*/
public static String extract(InputStream inputStream) throws Exception {
BufferedImage waterImg = ImageIO.read(inputStream);
int width = waterImg.getWidth();
int height = waterImg.getHeight();
double[][] grayMatrix = imgToGrayMatrix(waterImg);
boolean[] bits = new boolean[MAX_BIT_LENGTH];
int bitCursor = 0;
for (int y = 0; y <= height - BLOCK_SIZE && bitCursor < MAX_BIT_LENGTH; y += BLOCK_SIZE) {
for (int x = 0; x <= width - BLOCK_SIZE && bitCursor < MAX_BIT_LENGTH; x += BLOCK_SIZE) {
double[][] block = getBlock(grayMatrix, x, y);
double[][] dctBlock = dctForward(block);
// 读取中频系数判断比特 0/1
bits[bitCursor] = extractSingleBit(dctBlock);
bitCursor++;
}
}
// 二进制转回字符串
return bitsToText(bits, bitCursor);
}
//==================== 底层工具方法 ====================
/** 图片转灰度二维矩阵 */
private static double[][] imgToGrayMatrix(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
double[][] matrix = new double[h][w];
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
Color color = new Color(img.getRGB(x, y));
// 标准灰度计算公式
matrix[y][x] = color.getRed() * 0.299 + color.getGreen() * 0.587 + color.getBlue() * 0.114;
}
}
return matrix;
}
/** 灰度矩阵还原为图片 */
private static BufferedImage grayMatrixToImg(double[][] matrix, int width, int height) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 限制像素区间 0~255
int gray = (int) Math.max(0, Math.min(255, matrix[y][x]));
img.setRGB(x, y, new Color(gray, gray, gray).getRGB());
}
}
return img;
}
/** 截取 8*8 像素块 */
private static double[][] getBlock(double[][] matrix, int startX, int startY) {
double[][] block = new double[BLOCK_SIZE][BLOCK_SIZE];
for (int i = 0; i < BLOCK_SIZE; i++) {
System.arraycopy(matrix[startY + i], startX, block[i], 0, BLOCK_SIZE);
}
return block;
}
/** 写入 8*8 像素块 */
private static void setBlock(double[][] matrix, int startX, int startY, double[][] block) {
for (int i = 0; i < BLOCK_SIZE; i++) {
System.arraycopy(block[i], 0, matrix[startY + i], startX, BLOCK_SIZE);
}
}
/** DCT 正变换 */
private static double[][] dctForward(double[][] block) {
double[][] res = new double[BLOCK_SIZE][BLOCK_SIZE];
double c0 = Math.sqrt(1.0 / BLOCK_SIZE);
double ci = Math.sqrt(2.0 / BLOCK_SIZE);
for (int u = 0; u < BLOCK_SIZE; u++) {
for (int v = 0; v < BLOCK_SIZE; v++) {
double sum = 0;
for (int x = 0; x < BLOCK_SIZE; x++) {
for (int y = 0; y < BLOCK_SIZE; y++) {
sum += block[x][y]
* Math.cos((2.0 * x + 1) * u * Math.PI / (2.0 * BLOCK_SIZE))
* Math.cos((2.0 * y + 1) * v * Math.PI / (2.0 * BLOCK_SIZE));
}
}
double cu = u == 0 ? c0 : ci;
double cv = v == 0 ? c0 : ci;
res[u][v] = cu * cv * sum;
}
}
return res;
}
/** DCT 逆变换 */
private static double[][] dctInverse(double[][] block) {
double[][] res = new double[BLOCK_SIZE][BLOCK_SIZE];
double c0 = Math.sqrt(1.0 / BLOCK_SIZE);
double ci = Math.sqrt(2.0 / BLOCK_SIZE);
for (int x = 0; x < BLOCK_SIZE; x++) {
for (int y = 0; y < BLOCK_SIZE; y++) {
double sum = 0;
for (int u = 0; u < BLOCK_SIZE; u++) {
for (int v = 0; v < BLOCK_SIZE; v++) {
double cu = u == 0 ? c0 : ci;
double cv = v == 0 ? c0 : ci;
sum += cu * cv * block[u][v]
* Math.cos((2.0 * x + 1) * u * Math.PI / (2.0 * BLOCK_SIZE))
* Math.cos((2.0 * y + 1) * v * Math.PI / (2.0 * BLOCK_SIZE));
}
}
res[x][y] = sum;
}
}
return res;
}
/** 嵌入单个比特:修改中频 (4,4) 系数 */
private static void embedSingleBit(double[][] dctBlock, boolean bit) {
if (bit) {
dctBlock[4][4] += STRENGTH;
} else {
dctBlock[4][4] -= STRENGTH;
}
}
/** 提取单个比特:判断系数正负 */
private static boolean extractSingleBit(double[][] dctBlock) {
return dctBlock[4][4] > 0;
}
// 文本转二进制比特数组
private static boolean[] textToBits(String text) {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
int totalBit = bytes.length * 8;
if (totalBit > MAX_BIT_LENGTH) {
throw new RuntimeException("水印文字过长,请缩短内容");
}
boolean[] bits = new boolean[totalBit];
int idx = 0;
for (byte b : bytes) {
for (int i = 7; i >= 0; i--) {
bits[idx++] = ((b >> i) & 1) == 1;
}
}
return bits;
}
// 二进制比特转回文本
private static String bitsToText(boolean[] bits, int validLen) {
int byteNum = validLen / 8;
byte[] bytes = new byte[byteNum];
for (int i = 0; i < byteNum; i++) {
byte b = 0;
for (int j = 0; j < 8; j++) {
if (bits[i * 8 + j]) {
b |= (1 << (7 - j));
}
}
bytes[i] = b;
}
return new String(bytes, StandardCharsets.UTF_8);
}
}
四、Spring Boot 接口编写
提供两个接口:上传图片嵌入盲水印、上传水印图片提取内容。
java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
@RestController
public class WatermarkController {
// 临时文件存储目录
private static final String TEMP_DIR = System.getProperty("user.dir") + "/watermark_temp/";
// 项目启动自动创建文件夹
static {
try {
Files.createDirectories(Paths.get(TEMP_DIR));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 上传原图,嵌入 DCT 盲水印
* @param file 原图文件
* @param watermark 水印文字
* @return 结果+文件路径
*/
@PostMapping("/watermark/embed")
public String embedWatermark(@RequestParam MultipartFile file,
@RequestParam String watermark) {
try {
// 生成唯一文件名
String fileName = System.currentTimeMillis() + ".jpg";
String savePath = TEMP_DIR + fileName;
// 执行嵌入
DctBlindWatermarkUtil.embed(file.getInputStream(), savePath, watermark);
return "✅ 盲水印嵌入成功\n文件地址:" + savePath + "\n水印内容:" + watermark;
} catch (Exception e) {
e.printStackTrace();
return "❌ 水印嵌入失败:" + e.getMessage();
}
}
/**
* 上传带水印图片,提取盲水印
*/
@PostMapping("/watermark/extract")
public String extractWatermark(@RequestParam MultipartFile file) {
try {
String content = DctBlindWatermarkUtil.extract(file.getInputStream());
return "✅ 提取水印成功:" + content;
} catch (Exception e) {
e.printStackTrace();
return "❌ 水印提取失败:" + e.getMessage();
}
}
}
五、上传文件配置
解决大图片上传超限:
yaml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 15MB
六、接口测试方式
方式 1:Postman 测试
嵌入水印:
| 项目 | 值 |
|---|---|
| 请求方式 | POST |
| 地址 | http://localhost:8080/watermark/embed |
| 参数 file | 选择本地图片 |
| 参数 watermark | 自定义水印文字,例如 `版权归属-XX公司 |
提取水印:
| 项目 | 值 |
|---|---|
| 请求方式 | POST |
| 地址 | http://localhost:8080/watermark/extract |
| 参数 file | 上传刚刚生成的带盲水印图片 |
方式 2:curl 命令测试
bash
# 嵌入水印
curl -X POST -F "file=@/本地图片路径/test.jpg" \
-F "watermark=我的图片版权水印" \
http://localhost:8080/watermark/embed
# 提取水印
curl -X POST -F "file=@/生成的水印图片路径" \
http://localhost:8080/watermark/extract
七、参数调优与注意事项
7.1 水印强度 STRENGTH
#mermaid-svg-mJiYBtcb0BTPQYLH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mJiYBtcb0BTPQYLH .error-icon{fill:#552222;}#mermaid-svg-mJiYBtcb0BTPQYLH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mJiYBtcb0BTPQYLH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mJiYBtcb0BTPQYLH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mJiYBtcb0BTPQYLH .marker.cross{stroke:#333333;}#mermaid-svg-mJiYBtcb0BTPQYLH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mJiYBtcb0BTPQYLH p{margin:0;}#mermaid-svg-mJiYBtcb0BTPQYLH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster-label text{fill:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster-label span{color:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster-label span p{background-color:transparent;}#mermaid-svg-mJiYBtcb0BTPQYLH .label text,#mermaid-svg-mJiYBtcb0BTPQYLH span{fill:#333;color:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH .node rect,#mermaid-svg-mJiYBtcb0BTPQYLH .node circle,#mermaid-svg-mJiYBtcb0BTPQYLH .node ellipse,#mermaid-svg-mJiYBtcb0BTPQYLH .node polygon,#mermaid-svg-mJiYBtcb0BTPQYLH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mJiYBtcb0BTPQYLH .rough-node .label text,#mermaid-svg-mJiYBtcb0BTPQYLH .node .label text,#mermaid-svg-mJiYBtcb0BTPQYLH .image-shape .label,#mermaid-svg-mJiYBtcb0BTPQYLH .icon-shape .label{text-anchor:middle;}#mermaid-svg-mJiYBtcb0BTPQYLH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mJiYBtcb0BTPQYLH .rough-node .label,#mermaid-svg-mJiYBtcb0BTPQYLH .node .label,#mermaid-svg-mJiYBtcb0BTPQYLH .image-shape .label,#mermaid-svg-mJiYBtcb0BTPQYLH .icon-shape .label{text-align:center;}#mermaid-svg-mJiYBtcb0BTPQYLH .node.clickable{cursor:pointer;}#mermaid-svg-mJiYBtcb0BTPQYLH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mJiYBtcb0BTPQYLH .arrowheadPath{fill:#333333;}#mermaid-svg-mJiYBtcb0BTPQYLH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mJiYBtcb0BTPQYLH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mJiYBtcb0BTPQYLH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mJiYBtcb0BTPQYLH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mJiYBtcb0BTPQYLH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mJiYBtcb0BTPQYLH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster text{fill:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH .cluster span{color:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mJiYBtcb0BTPQYLH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mJiYBtcb0BTPQYLH rect.text{fill:none;stroke-width:0;}#mermaid-svg-mJiYBtcb0BTPQYLH .icon-shape,#mermaid-svg-mJiYBtcb0BTPQYLH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mJiYBtcb0BTPQYLH .icon-shape p,#mermaid-svg-mJiYBtcb0BTPQYLH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mJiYBtcb0BTPQYLH .icon-shape .label rect,#mermaid-svg-mJiYBtcb0BTPQYLH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mJiYBtcb0BTPQYLH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mJiYBtcb0BTPQYLH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mJiYBtcb0BTPQYLH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} STRENGTH 值
8~10
几乎完全看不见
JPG 压缩后易失败
12~16
均衡首选
轻微压缩/缩放仍可提取
18
痕迹明显
失去盲水印意义
| 强度区间 | 隐蔽性 | 鲁棒性 |
|---|---|---|
| 8 ~ 10 | 几乎完全看不见 | JPG 压缩后容易提取失败 |
| 12 ~ 16 | 轻微压缩、缩放依旧可提取 | 近距离细看有极其淡的色块 |
| 18 | 水印痕迹明显 | 失去盲水印意义 |
7.2 图片格式适配
| 格式 | 说明 |
|---|---|
| JPG、PNG(推荐) | PNG 无损格式水印留存效果最好 |
| gif 动图(不支持) | 需要逐帧处理 |
7.3 缺陷与生产优化
| 问题 | 优化方案 |
|---|---|
| 原生手写 DCT 运算性能一般 | 大图处理建议异步线程池 + 消息队列,避免接口超时 |
| 仅灰度图嵌入 | 如需 RGB 三色通道水印(抗篡改更强),分别对 R/G/B 三个通道各自做 DCT 嵌入 |
| 商用生产方案 | 自研 DCT 适合学习演示;正式业务推荐开源成熟水印框架,或阿里云、腾讯云图片盲水印 API(抗截图、抗涂抹、批量溯源) |
7.4 常见异常排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 提取水印乱码 | 强度过低、图片被重度压缩裁剪 | 调高 STRENGTH |
| 图片整体发灰 | 灰度转换逻辑正常 | RGB 三通道嵌入可规避灰度问题 |
| 文字超长报错 | 超过 MAX_BIT_LENGTH |
缩短水印内容或扩大 MAX_BIT_LENGTH |
八、总结
#mermaid-svg-ZcZE791JAnL0yuOX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZcZE791JAnL0yuOX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZcZE791JAnL0yuOX .error-icon{fill:#552222;}#mermaid-svg-ZcZE791JAnL0yuOX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZcZE791JAnL0yuOX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZcZE791JAnL0yuOX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZcZE791JAnL0yuOX .marker.cross{stroke:#333333;}#mermaid-svg-ZcZE791JAnL0yuOX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZcZE791JAnL0yuOX p{margin:0;}#mermaid-svg-ZcZE791JAnL0yuOX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZcZE791JAnL0yuOX .cluster-label text{fill:#333;}#mermaid-svg-ZcZE791JAnL0yuOX .cluster-label span{color:#333;}#mermaid-svg-ZcZE791JAnL0yuOX .cluster-label span p{background-color:transparent;}#mermaid-svg-ZcZE791JAnL0yuOX .label text,#mermaid-svg-ZcZE791JAnL0yuOX span{fill:#333;color:#333;}#mermaid-svg-ZcZE791JAnL0yuOX .node rect,#mermaid-svg-ZcZE791JAnL0yuOX .node circle,#mermaid-svg-ZcZE791JAnL0yuOX .node ellipse,#mermaid-svg-ZcZE791JAnL0yuOX .node polygon,#mermaid-svg-ZcZE791JAnL0yuOX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZcZE791JAnL0yuOX .rough-node .label text,#mermaid-svg-ZcZE791JAnL0yuOX .node .label text,#mermaid-svg-ZcZE791JAnL0yuOX .image-shape .label,#mermaid-svg-ZcZE791JAnL0yuOX .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZcZE791JAnL0yuOX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZcZE791JAnL0yuOX .rough-node .label,#mermaid-svg-ZcZE791JAnL0yuOX .node .label,#mermaid-svg-ZcZE791JAnL0yuOX .image-shape .label,#mermaid-svg-ZcZE791JAnL0yuOX .icon-shape .label{text-align:center;}#mermaid-svg-ZcZE791JAnL0yuOX .node.clickable{cursor:pointer;}#mermaid-svg-ZcZE791JAnL0yuOX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZcZE791JAnL0yuOX .arrowheadPath{fill:#333333;}#mermaid-svg-ZcZE791JAnL0yuOX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZcZE791JAnL0yuOX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZcZE791JAnL0yuOX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZcZE791JAnL0yuOX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZcZE791JAnL0yuOX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZcZE791JAnL0yuOX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZcZE791JAnL0yuOX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZcZE791JAnL0yuOX .cluster text{fill:#333;}#mermaid-svg-ZcZE791JAnL0yuOX .cluster span{color:#333;}#mermaid-svg-ZcZE791JAnL0yuOX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZcZE791JAnL0yuOX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZcZE791JAnL0yuOX rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZcZE791JAnL0yuOX .icon-shape,#mermaid-svg-ZcZE791JAnL0yuOX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZcZE791JAnL0yuOX .icon-shape p,#mermaid-svg-ZcZE791JAnL0yuOX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZcZE791JAnL0yuOX .icon-shape .label rect,#mermaid-svg-ZcZE791JAnL0yuOX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZcZE791JAnL0yuOX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZcZE791JAnL0yuOX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZcZE791JAnL0yuOX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 能力
肉眼无感
抗压缩
抗轻微裁剪
版权溯源
Spring Boot 盲水印方案
DCT 频域算法
灰度转换
8×8 分块 DCT
中频系数嵌入
逆 DCT 还原
本方案基于 DCT 频域算法实现了图片盲水印的完整链路:
| 要点 | 说明 |
|---|---|
| 算法选择 | DCT 频域水印鲁棒性优于 LSB,JPG 压缩、缩放、小幅裁剪后仍可解析 |
| 嵌入位置 | 中频系数 (4,4),兼顾隐蔽性与抗压缩能力 |
| 强度调节 | STRENGTH 参数在隐蔽性与鲁棒性之间权衡,推荐 12~16 |
| 生产建议 | 大图异步处理,商用场景考虑成熟水印框架或云 API |
适用场景:版权溯源、图片溯源、内容防伪、用户 ID 隐写等。