Spring Boot 实现图片盲水印:基于 DCT 频域算法的隐形数字水印

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 还原像素块
生成带盲水印图片

关键设计:

  1. 将彩色图片转为灰度图像,划分成无数 8×8 像素小块
  2. 对每个像素块做 DCT 变换,把空间像素转为频率系数
  3. 在中频系数嵌入水印比特(修改系数大小):肉眼无法察觉,兼顾隐蔽性;压缩、轻微裁剪后依旧可以提取水印
  4. 提取时重新 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 隐写等。

相关推荐
晴空蓝天1 小时前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python
Zane19941 小时前
贪心算法大多靠不住,为什么Dijkstra的贪心选择偏偏能保证全局最优
算法
CQU_JIAKE1 小时前
9.26【A】
算法
-cywen-2 小时前
GROUNDHOG总体版
人工智能·深度学习·算法
_不会dp不改名_2 小时前
leetcode_1614嵌套括号的最大深度
算法·leetcode·职场和发展
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章110-YOLO 实例分割
图像处理·人工智能·opencv·算法·yolo·计算机视觉
卓怡学长2 小时前
w203基于springboot钢材仓库管理系统的设计与实现
java·spring boot·spring·intellij-idea
sunburn-2 小时前
Java 排序算法详细教学:从冒泡排序到快速排序
java·开发语言·数据结构·ide·算法
阳明山水2 小时前
因果嵌入与流水线范式的本质差异
人工智能·深度学习·算法·机器学习·架构