乐于分享
好东西不私藏

被豆包AI水印烦透了,我干脆自己写了个工具

被豆包AI水印烦透了,我干脆自己写了个工具

被豆包AI水印烦透了,我干脆自己写了个工具

平时用豆包AI搞图挺香的,写段提示词等几秒就出图,效率确实高。

但有个问题一直挺烦人的——每张图右下角都会带个豆包角标。自己看看还行,想做公众号配图、PPT素材或者设计稿的时候,这个水印就很尴尬了,直接让图废了一半。

之前也试过一些去水印工具,要么满屏广告,要么要充会员,要么处理完画面糊成一片,修复痕迹比水印本身还显眼。折腾了几次之后,干脆自己动手写了一个。反正也是写代码的,这事儿对我来说其实不算复杂。

效果先看

左边是豆包原图,右下角有角标水印;右边是处理后,水印没了,背景也填得挺自然,基本看不出修过。

整个过程就四步:打开程序、选文件夹、点一个按钮、等进度条走完。几十张图大概也就几十秒的事儿,比手动一张张修快太多了。

能干什么

程序名字叫 WatermarkRemoverUltimate,纯 Java 写的,带图形界面,不用敲命令行。

其实就两个功能,但覆盖了大部分场景:

一键批量去 AI 右下角水印

这个是我最常用的。豆包的水印基本都在右下角,程序会自动定位到那个区域(大概是图片宽高的右下角 20%),然后智能填充修复,整个文件夹的图片可以一次性处理完。

手动框选去水印

如果水印位置不固定,或者你想处理其他类型的水印,可以选单张图片,在预览界面拖拽框选水印位置,然后批量处理。

普通用户怎么用

需要的环境: 电脑上装了 Java 就行,Win/Mac/Linux 都能跑。没装的话去 java.com 下载一个,安装过程基本就是一路下一步。

第一步:把代码跑起来

你可以直接复制文章后面的完整代码,保存成 WatermarkRemoverUltimate.java,然后在同目录下运行:

javac WatermarkRemoverUltimate.java

java WatermarkRemoverUltimate

运行之后就会弹出这样一个界面:

界面很简单,就三个区域:上面选文件夹,中间选处理模式,下面是预览和操作状态。

第二步:选输入和输出文件夹

点「📁 选择待处理图片文件夹」,选中你存放豆包图片的文件夹。输出文件夹可以另选一个空的,也可以不选——程序会自动在输入目录下建一个 auto_output 文件夹放处理结果。

第三步:点一键处理

直接点那个蓝色的「🔥 一键批量去除AI右下角水印」按钮,等进度条跑完,打开输出文件夹就能看到处理好的图片了。

说实话,这个流程我设计的时候就想尽量简单,最好是家里长辈也能用的那种程度。

技术人可能关心的实现细节

去水印本质上是 图像修复(Inpainting),就是拿水印周围的像素信息去推测、填充被遮挡的区域。

这个工具用的是一套纯像素级的多方向采样融合算法,没有调任何深度学习模型,纯 Java 标准库搞定。好处是不用下几十上百兆的模型文件,坏处是效果肯定不如 AI 模型那么精致,但处理豆包这种角标水印已经够用了。

修复算法的核心逻辑

public static BufferedImage highLevelAiInpaint(BufferedImage src, int x, int y, int w, int h) {

BufferedImageimg = copyImage(src);

intwidth = img.getWidth();

intheight = img.getHeight();

intstartX = Math.max(5, x);

intstartY = Math.max(5, y);

intendX = Math.min(width - 5, x + w);

intendY = Math.min(height - 5, y + h);

intsampleNear = 8;// 近邻采样距离

intsampleFar = 20;// 远邻采样距离

for(int dy = startY; dy < endY; dy++) {

for(int dx = startX; dx < endX; dx++) {

//上下左右各两圈像素采样

intn1 = getSafeRgb(img, dx, dy - sampleNear, width, height);

intn2 = getSafeRgb(img, dx, dy + sampleNear, width, height);

intn3 = getSafeRgb(img, dx - sampleNear, dy, width, height);

intn4 = getSafeRgb(img, dx + sampleNear, dy, width, height);

intf1 = getSafeRgb(img, dx, dy - sampleFar, width, height);

intf2 = getSafeRgb(img, dx, dy + sampleFar, width, height);

intf3 = getSafeRgb(img, dx - sampleFar, dy, width, height);

intf4 = getSafeRgb(img, dx + sampleFar, dy, width, height);

//近邻权重 60%,远邻权重 40%

intr = (int) (nearAvg(n1,n2,n3,n4,'r') * 0.6 + farAvg(f1,f2,f3,f4,'r') * 0.4);

intg = (int) (nearAvg(n1,n2,n3,n4,'g') * 0.6 + farAvg(f1,f2,f3,f4,'g') * 0.4);

intb = (int) (nearAvg(n1,n2,n3,n4,'b') * 0.6 + farAvg(f1,f2,f3,f4,'b') * 0.4);

//边缘渐变融合,避免修复区域出现硬边界

doublerate = getEdgeGradientRate(dx - startX, dy - startY, w, h);

r= (int) (r * rate + getR(img.getRGB(dx, dy)) * (1 - rate));

g= (int) (g * rate + getG(img.getRGB(dx, dy)) * (1 - rate));

b= (int) (b * rate + getB(img.getRGB(dx, dy)) * (1 - rate));

img.setRGB(dx,dy, (0xff << 24) | (r << 16) | (g << 8) | b);

}

}

returnimg;

}

算法思路其实挺简单的,三句话就能说清:

对水印区域内每个像素,向上下左右各采两圈颜色——近邻采 8px,远邻采 20px

近邻颜色占 60% 权重,远邻占 40%,加权平均算出填充色。近的地方颜色更准,远的地方能补充一些背景的整体趋势

边缘区域用 getEdgeGradientRate 做渐变过渡,不然修复区域和原图衔接的地方会有一道硬边,看着很假

这个算法对背景简单的图效果挺好——纯色、渐变、模糊虚化背景基本都能无痕修复。但背景复杂的情况(比如水印下面恰好是人脸、细密纹理或者规律图案),效果会打点折扣。日常使用够用了,毕竟豆包生成的图大部分背景也不会太复杂。

右下角水印的自动定位

// 自动计算右下角水印区域(图片宽高的 20%)

int ww = (int) (w * AI_WATERMARK_SCALE);// AI_WATERMARK_SCALE = 0.2

int wh = (int) (h * AI_WATERMARK_SCALE);

int wx = w - ww;// 从右边开始

int wy = h - wh;// 从下边开始

豆包的水印位置基本固定在右下角,所以程序直接按比例(宽高各 20%)自动定位,不需要你手动标坐标。这也是批量模式能无脑处理的关键——你只管选文件夹,剩下的事程序自己搞定。

完整代码

代码已经放在 Gitee 上了,可以直接访问:

👉[https://gitee.com/busyboluo/common-tools/blob/master/WatermarkRemoverUltimate.java](https://gitee.com/busyboluo/common-tools/blob/master/WatermarkRemoverUltimate.java)

不想点链接的话,下面就是完整代码,直接复制保存即可。没有第三方依赖,Java 8 及以上都能编译运行:

package com.ruoyi.common.utils;

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Dimension;

import java.awt.FlowLayout;

import java.awt.Font;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.GridLayout;

import java.awt.RenderingHints;

import java.awt.event.MouseAdapter;

import java.awt.event.MouseEvent;

import java.awt.image.BufferedImage;

import java.io.File;

import java.io.IOException;

import java.util.List;

import javax.imageio.ImageIO;

import javax.swing.*;

public class WatermarkRemoverUltimate {

privatestatic String batchInputDir = "";

privatestatic String batchOutputDir = "";

privatestatic int selectX, selectY, selectW, selectH;

privatestatic boolean isSelected = false;

privatestatic JFrame mainFrame;

privatestatic JProgressBar progressBar;

privatestatic JLabel statusLabel;

privatestatic JLabel inputPathLabel;

privatestatic JLabel outputPathLabel;

privatestatic BufferedImage currentImage;

privatestatic File currentImgFile;

privatestatic JPanel mainContentPane;

privatestatic final double AI_WATERMARK_SCALE = 0.2;

publicstatic void main(String[] args) {

SwingUtilities.invokeLater(WatermarkRemoverUltimate::initUI);

}

privatestatic void initUI() {

mainFrame= new JFrame("AI智能去水印工具");

mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

mainFrame.setSize(1050,800);

mainFrame.setLocationRelativeTo(null);

mainContentPane= new JPanel();

mainContentPane.setLayout(newBoxLayout(mainContentPane, BoxLayout.Y_AXIS));

mainContentPane.setBackground(Color.WHITE);

mainFrame.setContentPane(mainContentPane);

//文件夹选择面板

JPaneldirPanel = new JPanel(new GridLayout(2, 3, 15, 12));

dirPanel.setBorder(BorderFactory.createTitledBorder("第一步:批量文件夹选择(必选)"));

dirPanel.setBackground(Color.WHITE);

dirPanel.setPreferredSize(newDimension(1000, 100));

dirPanel.setMaximumSize(newDimension(1200, 100));

JButtonbtnSelectInput = new JButton("📁 选择待处理图片文件夹");

JButtonbtnSelectOutput = new JButton("📂 选择保存输出文件夹");

inputPathLabel= new JLabel("未选择输入文件夹", SwingConstants.LEFT);

outputPathLabel= new JLabel("未选择输出文件夹", SwingConstants.LEFT);

styleDirButton(btnSelectInput);

styleDirButton(btnSelectOutput);

inputPathLabel.setFont(newFont("微软雅黑", Font.PLAIN, 13));

outputPathLabel.setFont(newFont("微软雅黑", Font.PLAIN, 13));

dirPanel.add(btnSelectInput);dirPanel.add(inputPathLabel); dirPanel.add(new JLabel(""));

dirPanel.add(btnSelectOutput);dirPanel.add(outputPathLabel); dirPanel.add(new JLabel(""));

mainContentPane.add(dirPanel);

mainContentPane.add(Box.createVerticalStrut(10));

//功能按钮

JPanelbtnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 18, 12));

btnPanel.setBackground(Color.WHITE);

btnPanel.setBorder(BorderFactory.createTitledBorder("第二步:选择处理模式执行操作"));

btnPanel.setPreferredSize(newDimension(1000, 80));

btnPanel.setMaximumSize(newDimension(1200, 80));

JButtonbtnOpenImg = new JButton("1. 选图手动框选水印");

JButtonbtnSingleProcess = new JButton("2. 单张自定义去水印");

JButtonbtnBatchProcess = new JButton("3. 批量自定义去水印");

JButtonbtnAutoAiWatermark = new JButton("🔥 一键批量去除AI右下角水印");

styleButton(btnOpenImg);styleButton(btnSingleProcess); styleButton(btnBatchProcess); styleButton(btnAutoAiWatermark);

btnAutoAiWatermark.setBackground(newColor(60, 120, 255));

btnAutoAiWatermark.setForeground(Color.WHITE);

btnPanel.add(btnOpenImg);btnPanel.add(btnSingleProcess);

btnPanel.add(btnBatchProcess);btnPanel.add(btnAutoAiWatermark);

mainContentPane.add(btnPanel);

mainContentPane.add(Box.createVerticalStrut(10));

//图片预览区

JPanelimgPanel = new JPanel(new BorderLayout());

imgPanel.setBorder(BorderFactory.createTitledBorder("图片预览& 手动框选区域"));

imgPanel.setBackground(Color.WHITE);

imgPanel.setPreferredSize(newDimension(1000, 520));

JLabelimgTipLabel = new JLabel("自动模式:选好文件夹直接点一键去除AI水印 | 手动模式:选图拖拽框选", SwingConstants.CENTER);

imgTipLabel.setFont(newFont("微软雅黑", Font.PLAIN, 16));

imgPanel.add(imgTipLabel,BorderLayout.CENTER);

mainContentPane.add(imgPanel);

mainContentPane.add(Box.createVerticalStrut(10));

//状态栏

JPanelbottomPanel = new JPanel(new BorderLayout(15, 5));

bottomPanel.setBorder(BorderFactory.createEmptyBorder(12,15, 12, 15));

bottomPanel.setPreferredSize(newDimension(1000, 50));

statusLabel= new JLabel("就绪 | 请先选择待处理图片文件夹");

statusLabel.setFont(newFont("微软雅黑", Font.PLAIN, 14));

progressBar= new JProgressBar(0, 100);

progressBar.setPreferredSize(newDimension(320, 26));

progressBar.setStringPainted(true);

progressBar.setVisible(false);

bottomPanel.add(statusLabel,BorderLayout.WEST);

bottomPanel.add(progressBar,BorderLayout.EAST);

mainContentPane.add(bottomPanel);

btnSelectInput.addActionListener(e-> selectInputDir());

btnSelectOutput.addActionListener(e-> selectOutputDir());

btnOpenImg.addActionListener(e-> openAndSelectImage());

btnSingleProcess.addActionListener(e-> startSingleProcess());

btnBatchProcess.addActionListener(e-> startBatchProcess());

btnAutoAiWatermark.addActionListener(e-> startAutoAiWatermarkRemove());

mainFrame.setVisible(true);

}

privatestatic void styleDirButton(JButton btn) {

btn.setFont(newFont("微软雅黑", Font.PLAIN, 14));

btn.setPreferredSize(newDimension(200, 38));

btn.setFocusPainted(false);

}

privatestatic void styleButton(JButton btn) {

btn.setFont(newFont("微软雅黑", Font.PLAIN, 14));

btn.setPreferredSize(newDimension(175, 38));

btn.setFocusPainted(false);

}

privatestatic void selectInputDir() {

JFileChooserchooser = new JFileChooser();

chooser.setDialogTitle("选择【待处理图片文件夹】");

chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

if(chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {

batchInputDir= chooser.getSelectedFile().getAbsolutePath() + "/";

inputPathLabel.setText("✅已选输入:" + batchInputDir);

statusLabel.setText("✅输入文件夹配置完成");

}

}

privatestatic void selectOutputDir() {

JFileChooserchooser = new JFileChooser();

chooser.setDialogTitle("选择【处理后保存文件夹】");

chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

if(chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {

batchOutputDir= chooser.getSelectedFile().getAbsolutePath() + "/";

outputPathLabel.setText("✅已选输出:" + batchOutputDir);

statusLabel.setText("✅输出文件夹配置完成");

}

}

privatestatic String getSafeOutputPath() {

if(batchOutputDir.isEmpty()) {

batchOutputDir= batchInputDir.isEmpty()

?new File("").getAbsolutePath() + "/ai_watermark_output/"

:batchInputDir + "auto_output/";

}

FileoutDir = new File(batchOutputDir);

if(!outDir.exists()) outDir.mkdirs();

returnbatchOutputDir;

}

privatestatic void openAndSelectImage() {

JFileChooserchooser = new JFileChooser();

chooser.setDialogTitle("选择单张图片");

chooser.setFileFilter(newjavax.swing.filechooser.FileNameExtensionFilter("图片文件", "jpg", "jpeg", "png"));

if(chooser.showOpenDialog(mainFrame) != JFileChooser.APPROVE_OPTION) return;

currentImgFile= chooser.getSelectedFile();

try{ currentImage = ImageIO.read(currentImgFile); } catch (IOException e) {

JOptionPane.showMessageDialog(mainFrame,"图片读取失败!", "错误", JOptionPane.ERROR_MESSAGE); return;

}

JPanelpreviewPanel = new JPanel() {

finalBufferedImage img = currentImage;

intsx, sy, ex, ey;

{addMouseListener(new MouseAdapter() {

@Overridepublic void mousePressed(MouseEvent e) { sx = e.getX(); sy = e.getY(); }

@Overridepublic void mouseReleased(MouseEvent e) {

ex= e.getX(); ey = e.getY();

selectX= Math.min(sx, ex); selectY = Math.min(sy, ey);

selectW= Math.abs(ex - sx); selectH = Math.abs(ey - sy);

isSelected= true;

statusLabel.setText("✅水印框选完成");

JOptionPane.showMessageDialog(mainFrame,"水印区域框选成功!", "完成", JOptionPane.INFORMATION_MESSAGE);

repaint();

}

});}

@Overrideprotected void paintComponent(Graphics g) {

super.paintComponent(g);

if(img != null) {

doublescale = Math.min((double)getWidth()/img.getWidth(), (double)getHeight()/img.getHeight());

intdw = (int)(img.getWidth()*scale), dh = (int)(img.getHeight()*scale);

intox = (getWidth()-dw)/2, oy = (getHeight()-dh)/2;

g.drawImage(img,ox, oy, dw, dh, null);

if(isSelected) {

g.setColor(newColor(255,0,0,130));

g.fillRect(selectX+ox,selectY+oy, selectW, selectH);

g.setColor(Color.RED);

g.drawRect(selectX+ox,selectY+oy, selectW, selectH);

}

}

}

};

previewPanel.setBackground(Color.WHITE);

previewPanel.setPreferredSize(newDimension(1000, 520));

mainContentPane.remove(2);

mainContentPane.add(previewPanel,2);

mainFrame.revalidate();mainFrame.repaint();

}

privatestatic void startSingleProcess() {

if(!isSelected || currentImage == null) {

JOptionPane.showMessageDialog(mainFrame,"请先选图并手动框选水印!", "提示", JOptionPane.WARNING_MESSAGE); return;

}

newSwingWorker() {

@Overrideprotected Void doInBackground() throws Exception {

progressBar.setVisible(true);

statusLabel.setText("处理单张图片中...");

publish(30);

BufferedImageres = highLevelAiInpaint(currentImage, selectX, selectY, selectW, selectH);

publish(80);

Stringout = currentImgFile.getParent() + "/no_watermark_" + currentImgFile.getName();

saveImage(res,out);

publish(100);

statusLabel.setText("✅处理完成:" + out);

returnnull;

}

@Overrideprotected void process(Listc) { progressBar.setValue(c.get(c.size()-1)); }

@Overrideprotected void done() {

progressBar.setVisible(false);

JOptionPane.showMessageDialog(mainFrame,"✅ 单张去水印完成!", "成功", JOptionPane.INFORMATION_MESSAGE);

}

}.execute();

}

privatestatic void startBatchProcess() {

if(!isSelected) { JOptionPane.showMessageDialog(mainFrame, "请先手动框选水印区域!", "提示", JOptionPane.WARNING_MESSAGE); return; }

if(batchInputDir.isEmpty()) { JOptionPane.showMessageDialog(mainFrame, "请先选择待处理图片文件夹!", "错误", JOptionPane.ERROR_MESSAGE); return; }

FileinputDir = new File(batchInputDir);

File[]files = inputDir.listFiles((d,n) -> n.toLowerCase().matches(".*\\.(jpg|jpeg|png)"));

if(files == null || files.length == 0) { JOptionPane.showMessageDialog(mainFrame, "文件夹内无有效图片!", "提示", JOptionPane.WARNING_MESSAGE); return; }

StringsafeOut = getSafeOutputPath();

newSwingWorker() {

inttotal = files.length, finish = 0;

@Overrideprotected Void doInBackground() throws Exception {

progressBar.setVisible(true);

for(File f : files) {

statusLabel.setText("处理:"+ f.getName() + " (" + finish + "/" + total + ")");

BufferedImageimg = ImageIO.read(f);

if(img == null) { finish++; continue; }

saveImage(highLevelAiInpaint(img,selectX, selectY, selectW, selectH), safeOut + f.getName());

publish((int)((double)(++finish)/total*100));

Thread.sleep(30);

}

statusLabel.setText("✅批量处理完成,共" + finish + "张");

returnnull;

}

@Overrideprotected void process(Listc) { progressBar.setValue(c.get(c.size()-1)); }

@Overrideprotected void done() {

progressBar.setVisible(false);

JOptionPane.showMessageDialog(mainFrame,"✅ 批量处理完成!总计" + total + "张", "完成", JOptionPane.INFORMATION_MESSAGE);

}

}.execute();

}

privatestatic void startAutoAiWatermarkRemove() {

if(batchInputDir.isEmpty()) { JOptionPane.showMessageDialog(mainFrame, "请先选择【待处理图片文件夹】!", "错误", JOptionPane.ERROR_MESSAGE); return; }

FileinputDir = new File(batchInputDir);

File[]files = inputDir.listFiles((d,n) -> n.toLowerCase().matches(".*\\.(jpg|jpeg|png)"));

if(files == null || files.length == 0) { JOptionPane.showMessageDialog(mainFrame, "文件夹内无有效图片!", "提示", JOptionPane.WARNING_MESSAGE); return; }

StringsafeOut = getSafeOutputPath();

newSwingWorker() {

inttotal = files.length, finish = 0;

@Overrideprotected Void doInBackground() throws Exception {

progressBar.setVisible(true);

for(File f : files) {

statusLabel.setText("AI水印处理:"+ f.getName() + " (" + finish + "/" + total + ")");

BufferedImageimg = ImageIO.read(f);

if(img == null) { finish++; continue; }

intw = img.getWidth(), h = img.getHeight();

intww = (int)(w * AI_WATERMARK_SCALE), wh = (int)(h * AI_WATERMARK_SCALE);

saveImage(highLevelAiInpaint(img,w-ww, h-wh, ww, wh), safeOut + f.getName());

publish((int)((double)(++finish)/total*100));

Thread.sleep(20);

}

statusLabel.setText("✅AI水印全部去除完成,共" + finish + "张");

returnnull;

}

@Overrideprotected void process(Listc) { progressBar.setValue(c.get(c.size()-1)); }

@Overrideprotected void done() {

progressBar.setVisible(false);

JOptionPane.showMessageDialog(mainFrame,"✅ 一键去除完成!\n总数:" + total + "张\n保存目录:" + safeOut, "全自动完成", JOptionPane.INFORMATION_MESSAGE);

}

}.execute();

}

publicstatic BufferedImage highLevelAiInpaint(BufferedImage src, int x, int y, int w, int h) {

BufferedImageimg = copyImage(src);

intwidth = img.getWidth(), height = img.getHeight();

intstartX = Math.max(5, x), startY = Math.max(5, y);

intendX = Math.min(width-5, x+w), endY = Math.min(height-5, y+h);

intsampleNear = 8, sampleFar = 20;

for(int dy = startY; dy < endY; dy++) {

for(int dx = startX; dx < endX; dx++) {

intn1=getSafeRgb(img,dx,dy-sampleNear,width,height), n2=getSafeRgb(img,dx,dy+sampleNear,width,height);

intn3=getSafeRgb(img,dx-sampleNear,dy,width,height), n4=getSafeRgb(img,dx+sampleNear,dy,width,height);

intf1=getSafeRgb(img,dx,dy-sampleFar,width,height),f2=getSafeRgb(img,dx,dy+sampleFar,width,height);

intf3=getSafeRgb(img,dx-sampleFar,dy,width,height),f4=getSafeRgb(img,dx+sampleFar,dy,width,height);

intnr=(getR(n1)+getR(n2)+getR(n3)+getR(n4))/4, ng=(getG(n1)+getG(n2)+getG(n3)+getG(n4))/4, nb=(getB(n1)+getB(n2)+getB(n3)+getB(n4))/4;

intfr=(getR(f1)+getR(f2)+getR(f3)+getR(f4))/4, fg=(getG(f1)+getG(f2)+getG(f3)+getG(f4))/4, fb=(getB(f1)+getB(f2)+getB(f3)+getB(f4))/4;

intr=(int)(nr*0.6+fr*0.4), g=(int)(ng*0.6+fg*0.4), b=(int)(nb*0.6+fb*0.4);

doublerate = getEdgeGradientRate(dx-startX, dy-startY, w, h);

r=(int)(r*rate+getR(img.getRGB(dx,dy))*(1-rate));

g=(int)(g*rate+getG(img.getRGB(dx,dy))*(1-rate));

b=(int)(b*rate+getB(img.getRGB(dx,dy))*(1-rate));

img.setRGB(dx,dy, (0xff<<24)|(r<<16)|(g<<8)|b);

}

}

returnimg;

}

privatestatic int getSafeRgb(BufferedImage img, int x, int y, int mw, int mh) {

returnimg.getRGB(Math.max(0,Math.min(mw-1,x)), Math.max(0,Math.min(mh-1,y)));

}

privatestatic double getEdgeGradientRate(int x, int y, int w, int h) {

intedge = 6;

if(x > edge && y > edge && x < w-edge && y < h-edge) return 1.0;

returnMath.min(1.0, Math.min(Math.min(x,y), Math.min(w-x,h-y)) / (double)edge);

}

publicstatic BufferedImage copyImage(BufferedImage src) {

BufferedImagecopy = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB);

Graphics2Dg2d = copy.createGraphics();

g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);

g2d.drawImage(src,0, 0, null); g2d.dispose();

returncopy;

}

publicstatic void saveImage(BufferedImage img, String path) throws IOException {

ImageIO.write(img,path.toLowerCase().endsWith("png") ? "png" : "jpg", new File(path));

}

publicstatic int getR(int c) { return (c>>16)&0xff; }

publicstatic int getG(int c) { return (c>>8)&0xff; }

publicstatic int getB(int c) { return c&0xff; }

}

先泼点冷水:这事儿有局限

这个工具处理大多数豆包AI生成的图是没问题的,但有些情况确实搞不定,提前说清楚免得你用的时候失望:

背景复杂的图效果会打折:如果水印下面正好是人脸、细密纹理或者复杂建筑,纯像素采样修复会有明显失真。这种时候建议换其他工具,或者干脆不用这张图

隐形溯源水印去不掉:有些平台会在图片频域里嵌入肉眼看不见的水印,这个工具处理不了,它只能去掉肉眼可见的角标

商用请注意合规:去水印后的图片能不能商用取决于原平台的使用协议,这个工具只是技术分享,别拿它干违法的事

另外代码写得其实挺糙的,没有异常兜底,没有日志系统,Swing 代码也是一股脑堆在一起。如果有谁想拿去改进,欢迎 fork 或者给我提 issue。

如果你也在做 Java 图像处理,或者有更好的 inpainting 思路,欢迎在评论区留言。我写这玩意儿的时候其实偷了不少懒,能优化的地方肯定很多。

顺手转发一下呗,没准你朋友圈里也有人在被这个水印烦着。