惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
V
V2EX
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
A
About on SuperTechFans
博客园_首页
L
LangChain Blog
量子位
雷峰网
雷峰网
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Jina AI
Jina AI
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium

博客园 - 程序员李铁牛

赛事报名系统开发:角色与权限体系设计 赛事系统报名接龙高并发技术细节处理浅析 档案数字化研发手记:PDF合并内存溢出踩坑 纸质档案数字化管理系统研发手记:法院诉讼档案单套制落地案例复盘——从纸质卷宗到电子卷宗 档案数字化管理系统开发手记:人事档案专项审核与数字化的系统支撑——"凡提必审"下的技术要点 档案数字化研发手记:我们的技术栈选择——档案系统前后端选型理由(含一次"炫技"的教训) 社群团购系统开发分享——工程化(下):资金支付测试策略——资金模块 100% 覆盖,其他 60% 就够 社群团购系统类快团团源码开发——工程化(上):一套让 5 人小团队不出事故的开发规范 社群团购类快团团系统开发——RBAC 权限设计源码分析:当一个微信号有 4 种身份时怎么办 社群团购系统类快团团模式开发——微信小程序登录:code2Session 之后还有 5 件事要做 数字档案管理系统化研发手记:医院病历档案数字化——合规、病案首页 OCR 与调阅提速 数字档案系统研发手记:批量扫描任务调度——TWAIN/SANE 采集驱动的封装实践 档案数字化系统研发源码手记:置信度过滤——把 97% 识别率变成真正可交付的成果 档案数字化开发实例手记:盖章遮挡文字识别恢复 景区运营预约系统开发:景区会员体系怎么搭?3级会员模型+积分玩法 景区门票预约系统全渠道平台库存数据同步技术处理方式 系统宕机了怎么办?景区票务系统应急预案模板 景区门票预约系统开发:接入AI预测客流设计思路分析 景区预约系统开发总结:门票分时预约设计原理和落地方法 设备运维管理系统开发实战:用规则引擎实现可配置的设备告警策略自研轻量引擎 vs Drools 落地对比 设备维修保养系统预测性维护不用深度学习?设备健康度评分的务实实现方案 档案数字化管理系统研发手记:档案权限模型——全宗-门类-案卷-文件四级控制的设计与实践 档案管理信息化系统研发手记:数字水印方案对比——可见水印 vs 盲水印(档案借阅防泄露) 景区票务系统开发实例分析:上云还是本地部署?6个维度判断 档案数字化系统源码研发手记:老旧档案去污点——中值滤波与 inpainting 效果对比 一物一码防伪溯源系统开发全栈源码串联一次扫码的完整链路追踪代码走读 景区门票预约系统票务系统的6个核心模块 景区上线门票预约系统的5个常见翻车点 景区门票预约系统之人脸识别 vs 身份证核验 vs 扫码入园,哪种最适合你? 景区门票预约系统开发深度解析之定价策略功能设计
图片和视频去水印软件开发方案JAVA版本
程序员李铁牛 · 2025-11-29 · via 博客园 - 程序员李铁牛

1. 项目概述与环境配置

项目架构设计

本项目采用模块化设计,包含以下核心模块:

  • 图形用户界面(GUI)
  • 图像处理模块(基于OpenCV)
  • 视频处理模块(基于FFmpeg)
  • 文件管理模块

Maven依赖配置

<dependencies>
    <!-- OpenCV图像处理 -->
    <dependency>
        <groupId>org.openpnp</groupId>
        <artifactId>opencv</artifactId>
        <version>4.5.3-2</version>
    </dependency>
    
    <!-- JavaCV (FFmpeg封装) -->
    <dependency>
        <groupId>org.bytedeco</groupId>
        <artifactId>javacv-platform</artifactId>
        <version>1.5.7</version>
    </dependency>
    
    <!-- Swing图形界面 -->
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-controls</artifactId>
        <version>17</version>
    </dependency>
</dependencies>

2. 核心图像去水印实现

基于OpenCV的图像处理类

import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.photo.Photo;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;

public class ImageWatermarkRemover {
    private Mat sourceImage;
    private Mat resultImage;
    private Point startPoint = new Point();
    private Point endPoint = new Point();
    private boolean isSelecting = false;
    
    static {
        // 加载OpenCV本地库[2](@ref)
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }
    
    /**
     * 加载图像文件
     */
    public BufferedImage loadImage(String filePath) {
        try {
            sourceImage = Imgcodecs.imread(filePath);
            if (sourceImage.empty()) {
                JOptionPane.showMessageDialog(null, "无法加载图像文件");
                return null;
            }
            return matToBufferedImage(sourceImage);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "图像加载错误: " + e.getMessage());
            return null;
        }
    }
    
    /**
     * 设置水印区域坐标
     */
    public void setWatermarkRegion(int x, int y, int width, int height) {
        this.startPoint.x = x;
        this.startPoint.y = y;
        this.endPoint.x = x + width;
        this.endPoint.y = y + height;
    }
    
    /**
     * 使用图像修复算法去除水印[2,3](@ref)
     */
    public BufferedImage removeWatermark() {
        if (sourceImage == null) {
            JOptionPane.showMessageDialog(null, "请先加载图像");
            return null;
        }
        
        try {
            // 创建水印区域的掩码
            Mat mask = createMaskFromRegion();
            
            // 使用OpenCV的inpainting算法修复水印区域[2](@ref)
            resultImage = new Mat();
            Photo.inpaint(sourceImage, mask, resultImage, 3, Photo.INPAINT_TELEA);
            
            return matToBufferedImage(resultImage);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "去水印处理失败: " + e.getMessage());
            return null;
        }
    }
    
    /**
     * 根据选定的区域创建掩码
     */
    private Mat createMaskFromRegion() {
        Mat mask = Mat.zeros(sourceImage.size(), CvType.CV_8UC1);
        
        // 确保区域在图像范围内
        int x1 = (int) Math.max(0, Math.min(startPoint.x, sourceImage.width() - 1));
        int y1 = (int) Math.max(0, Math.min(startPoint.y, sourceImage.height() - 1));
        int x2 = (int) Math.max(0, Math.min(endPoint.x, sourceImage.width() - 1));
        int y2 = (int) Math.max(0, Math.min(endPoint.y, sourceImage.height() - 1));
        
        // 绘制矩形掩码区域
        Imgproc.rectangle(mask, 
            new Point(x1, y1), 
            new Point(x2, y2), 
            new Scalar(255), // 白色填充
            -1); // 实心矩形
        
        return mask;
    }
    
    /**
     * 保存处理后的图像
     */
    public boolean saveImage(String outputPath) {
        if (resultImage == null || resultImage.empty()) {
            JOptionPane.showMessageDialog(null, "没有可保存的处理结果");
            return false;
        }
        
        try {
            return Imgcodecs.imwrite(outputPath, resultImage);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "保存失败: " + e.getMessage());
            return false;
        }
    }
    
    /**
     * 将OpenCV的Mat转换为BufferedImage
     */
    private BufferedImage matToBufferedImage(Mat mat) {
        try {
            int type = BufferedImage.TYPE_BYTE_GRAY;
            if (mat.channels() > 1) {
                type = BufferedImage.TYPE_3BYTE_BGR;
            }
            
            int bufferSize = mat.channels() * mat.cols() * mat.rows();
            byte[] buffer = new byte[bufferSize];
            mat.get(0, 0, buffer);
            
            BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type);
            final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
            System.arraycopy(buffer, 0, targetPixels, 0, buffer.length);
            
            return image;
        } catch (Exception e) {
            System.err.println("图像转换错误: " + e.getMessage());
            return null;
        }
    }
}

3. 视频去水印实现

基于FFmpeg的视频处理类

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import javax.swing.*;
import java.io.File;

public class VideoWatermarkRemover {
    private String inputPath;
    private String outputPath;
    private int x, y, width, height;
    
    /**
     * 设置水印区域参数
     */
    public void setWatermarkRegion(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    
    /**
     * 使用FFmpeg去除视频水印[5](@ref)
     */
    public boolean removeVideoWatermark(String inputPath, String outputPath) {
        this.inputPath = inputPath;
        this.outputPath = outputPath;
        
        FFmpegFrameGrabber grabber = null;
        FFmpegFrameRecorder recorder = null;
        
        try {
            // 初始化帧抓取器
            grabber = new FFmpegFrameGrabber(inputPath);
            grabber.start();
            
            // 初始化帧录制器
            recorder = new FFmpegFrameRecorder(outputPath, 
                grabber.getImageWidth(), grabber.getImageHeight());
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setFormat("mp4");
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.start();
            
            Frame frame;
            Java2DFrameConverter converter = new Java2DFrameConverter();
            int frameCount = 0;
            
            // 进度显示
            JProgressBar progressBar = new JProgressBar(0, 100);
            JOptionPane.showMessageDialog(null, progressBar, "处理进度", 
                JOptionPane.INFORMATION_MESSAGE);
            
            // 逐帧处理
            while ((frame = grabber.grab()) != null) {
                if (frame.image != null) {
                    // 处理当前帧的水印
                    Frame processedFrame = processFrame(frame);
                    recorder.record(processedFrame);
                } else {
                    recorder.record(frame);
                }
                
                frameCount++;
                // 更新进度条
                int progress = (int) ((double) frameCount / grabber.getLengthInFrames() * 100);
                progressBar.setValue(progress);
            }
            
            return true;
            
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "视频处理错误: " + e.getMessage());
            return false;
        } finally {
            try {
                if (recorder != null) {
                    recorder.close();
                }
                if (grabber != null) {
                    grabber.close();
                }
            } catch (Exception e) {
                System.err.println("资源释放错误: " + e.getMessage());
            }
        }
    }
    
    /**
     * 使用delogo滤镜处理帧[5](@ref)
     */
    private Frame processFrame(Frame frame) {
        // 这里可以使用JavaCV的滤镜功能
        // 由于JavaCV的滤镜API较复杂,我们可以调用FFmpeg命令行
        // 或者使用以下替代方法:对选定区域进行模糊处理
        
        // 替代方案:使用高斯模糊覆盖水印区域
        // 实际项目中可以考虑使用更复杂的算法
        
        return frame;
    }
    
    /**
     * 通过系统调用FFmpeg命令去除水印(更高效的方法)[5](@ref)
     */
    public boolean removeWatermarkWithFFmpegCommand(String inputPath, String outputPath) {
        try {
            // 构建FFmpeg命令
            String command = String.format(
                "ffmpeg -i \"%s\" -vf \"delogo=x=%d:y=%d:w=%d:h=%d:show=0\" -c:a copy \"%s\"",
                inputPath, x, y, width, height, outputPath);
            
            ProcessBuilder processBuilder = new ProcessBuilder();
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                processBuilder.command("cmd.exe", "/c", command);
            } else {
                processBuilder.command("bash", "-c", command);
            }
            
            Process process = processBuilder.start();
            int exitCode = process.waitFor();
            
            return exitCode == 0;
            
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "FFmpeg处理错误: " + e.getMessage());
            return false;
        }
    }
}

4. 图形用户界面实现

主界面类

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;

public class WatermarkRemoverGUI extends JFrame {
    private ImageWatermarkRemover imageProcessor;
    private VideoWatermarkRemover videoProcessor;
    private JLabel imageLabel;
    private JButton selectImageBtn, selectVideoBtn, removeBtn, saveBtn;
    private JTextField xField, yField, widthField, heightField;
    private BufferedImage currentImage;
    private Rectangle selectedRegion;
    
    public WatermarkRemoverGUI() {
        initializeComponents();
        setupLayout();
        setupEventHandlers();
    }
    
    private void initializeComponents() {
        setTitle("Java去水印工具");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(800, 600);
        
        imageProcessor = new ImageWatermarkRemover();
        videoProcessor = new VideoWatermarkRemover();
        
        // 创建组件
        imageLabel = new JLabel("请选择图片或视频文件", SwingConstants.CENTER);
        imageLabel.setBorder(BorderFactory.createEtchedBorder());
        
        selectImageBtn = new JButton("选择图片");
        selectVideoBtn = new JButton("选择视频");
        removeBtn = new JButton("去除水印");
        saveBtn = new JButton("保存结果");
        
        xField = new JTextField("0", 5);
        yField = new JTextField("0", 5);
        widthField = new JTextField("100", 5);
        heightField = new JTextField("50", 5);
        
        // 初始化区域选择
        selectedRegion = new Rectangle(0, 0, 100, 50);
    }
    
    private void setupLayout() {
        setLayout(new BorderLayout());
        
        // 顶部控制面板
        JPanel controlPanel = new JPanel(new FlowLayout());
        controlPanel.add(new JLabel("X:"));
        controlPanel.add(xField);
        controlPanel.add(new JLabel("Y:"));
        controlPanel.add(yField);
        controlPanel.add(new JLabel("宽度:"));
        controlPanel.add(widthField);
        controlPanel.add(new JLabel("高度:"));
        controlPanel.add(heightField);
        
        controlPanel.add(selectImageBtn);
        controlPanel.add(selectVideoBtn);
        controlPanel.add(removeBtn);
        controlPanel.add(saveBtn);
        
        add(controlPanel, BorderLayout.NORTH);
        add(new JScrollPane(imageLabel), BorderLayout.CENTER);
        
        // 状态栏
        JLabel statusLabel = new JLabel("就绪");
        add(statusLabel, BorderLayout.SOUTH);
    }
    
    private void setupEventHandlers() {
        selectImageBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectImageFile();
            }
        });
        
        selectVideoBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectVideoFile();
            }
        });
        
        removeBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                removeWatermark();
            }
        });
        
        saveBtn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                saveResult();
            }
        });
        
        // 添加鼠标监听器用于选择区域
        imageLabel.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mousePressed(java.awt.event.MouseEvent evt) {
                startSelection(evt.getX(), evt.getY());
            }
            
            public void mouseReleased(java.awt.event.MouseEvent evt) {
                endSelection(evt.getX(), evt.getY());
            }
        });
        
        imageLabel.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            public void mouseDragged(java.awt.event.MouseEvent evt) {
                updateSelection(evt.getX(), evt.getY());
            }
        });
    }
    
    private void selectImageFile() {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(
            "图片文件", "jpg", "jpeg", "png", "bmp"));
        
        if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            currentImage = imageProcessor.loadImage(file.getAbsolutePath());
            if (currentImage != null) {
                displayImage(currentImage);
            }
        }
    }
    
    private void selectVideoFile() {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(
            "视频文件", "mp4", "avi", "mov", "wmv"));
        
        if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            JOptionPane.showMessageDialog(this, 
                "视频文件已选择: " + file.getName() + "\n请设置水印区域参数后点击去除水印");
        }
    }
    
    private void startSelection(int x, int y) {
        selectedRegion = new Rectangle(x, y, 0, 0);
        updateCoordinateFields();
    }
    
    private void updateSelection(int x, int y) {
        if (selectedRegion != null) {
            selectedRegion.width = x - selectedRegion.x;
            selectedRegion.height = y - selectedRegion.y;
            updateCoordinateFields();
            repaint(); // 重绘显示选择区域
        }
    }
    
    private void endSelection(int x, int y) {
        updateSelection(x, y);
    }
    
    private void updateCoordinateFields() {
        xField.setText(String.valueOf(selectedRegion.x));
        yField.setText(String.valueOf(selectedRegion.y));
        widthField.setText(String.valueOf(Math.abs(selectedRegion.width)));
        heightField.setText(String.valueOf(Math.abs(selectedRegion.height)));
    }
    
    private void removeWatermark() {
        try {
            int x = Integer.parseInt(xField.getText());
            int y = Integer.parseInt(yField.getText());
            int width = Integer.parseInt(widthField.getText());
            int height = Integer.parseInt(heightField.getText());
            
            imageProcessor.setWatermarkRegion(x, y, width, height);
            BufferedImage result = imageProcessor.removeWatermark();
            
            if (result != null) {
                displayImage(result);
                JOptionPane.showMessageDialog(this, "水印去除完成!");
            }
        } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(this, "请输入有效的区域参数");
        } catch (Exception ex) {
            JOptionPane.showMessageDialog(this, "处理错误: " + ex.getMessage());
        }
    }
    
    private void saveResult() {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setSelectedFile(new File("output.jpg"));
        
        if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            if (imageProcessor.saveImage(file.getAbsolutePath())) {
                JOptionPane.showMessageDialog(this, "保存成功!");
            }
        }
    }
    
    private void displayImage(BufferedImage image) {
        ImageIcon icon = new ImageIcon(image);
        imageLabel.setIcon(icon);
        imageLabel.setText("");
    }
    
    public static void main(String[] args) {
        // 设置系统样式
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new WatermarkRemoverGUI().setVisible(true);
            }
        });
    }
}

5. 高级功能与优化

批量处理功能

/**
 * 批量处理多个文件
 */
public class BatchWatermarkRemover {
    private ImageWatermarkRemover processor;
    
    public BatchWatermarkRemover() {
        processor = new ImageWatermarkRemover();
    }
    
    /**
     * 批量处理指定文件夹中的图片
     */
    public void batchProcessImages(String inputDir, String outputDir, 
                                  int x, int y, int width, int height) {
        File dir = new File(inputDir);
        File[] imageFiles = dir.listFiles((d, name) -> 
            name.toLowerCase().matches(".*\\.(jpg|jpeg|png|bmp)"));
        
        if (imageFiles == null || imageFiles.length == 0) {
            JOptionPane.showMessageDialog(null, "未找到图片文件");
            return;
        }
        
        JProgressBar progressBar = new JProgressBar(0, imageFiles.length);
        JDialog progressDialog = createProgressDialog(progressBar);
        
        new Thread(() -> {
            for (int i = 0; i < imageFiles.length; i++) {
                try {
                    File inputFile = imageFiles[i];
                    File outputFile = new File(outputDir, "processed_" + inputFile.getName());
                    
                    // 处理单个文件
                    processor.loadImage(inputFile.getAbsolutePath());
                    processor.setWatermarkRegion(x, y, width, height);
                    processor.removeWatermark();
                    processor.saveImage(outputFile.getAbsolutePath());
                    
                    // 更新进度
                    final int progress = i + 1;
                    SwingUtilities.invokeLater(() -> 
                        progressBar.setValue(progress));
                    
                } catch (Exception e) {
                    System.err.println("处理失败: " + imageFiles[i].getName() + 
                                     " - " + e.getMessage());
                }
            }
            
            SwingUtilities.invokeLater(() -> {
                progressDialog.dispose();
                JOptionPane.showMessageDialog(null, 
                    "批量处理完成! 处理了 " + imageFiles.length + " 个文件");
            });
        }).start();
        
        progressDialog.setVisible(true);
    }
    
    private JDialog createProgressDialog(JProgressBar progressBar) {
        JDialog dialog = new JDialog();
        dialog.setTitle("批量处理进度");
        dialog.setLayout(new BorderLayout());
        dialog.add(progressBar, BorderLayout.CENTER);
        dialog.setSize(300, 100);
        dialog.setLocationRelativeTo(null);
        dialog.setModal(true);
        return dialog;
    }
}

6. 使用说明和注意事项

环境配置要求

  1. ​​Java环境​​: JDK 8或更高版本
  2. ​​OpenCV本地库​​: 需要配置相应的本地库文件
  3. ​​FFmpeg​​: 视频处理需要安装FFmpeg到系统PATH中

使用步骤

  1. 启动应用程序
  2. 选择要处理的图片或视频文件
  3. 在图像上拖动鼠标选择水印区域,或手动输入坐标参数
  4. 点击"去除水印"按钮执行处理
  5. 预览结果并保存处理后的文件