图像透明处理

This commit is contained in:
spllzh
2025-10-16 23:21:39 +08:00
parent abd370beb5
commit cfcd04fe7e
33 changed files with 4658 additions and 23 deletions

View File

@@ -0,0 +1,454 @@
package com.rj.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.ArrayList;
/**
* 图像转换工具类
*
* 提供图像格式转换、背景移除、透明度处理等功能
* 专门用于满足阿里云背景生成API的图片要求
* - 主体图像必须为带透明背景的RGBA四通道图像
* - PNG格式长边不超过2048像素
* - 公网可访问的URL
*
* @author 成
* @date 2025/1/30
* @version 1.0
*/
@Slf4j
@Component
public class ImageConversionUtil {
/**
* 高级图片转换方法,专门处理透明背景需求
* 确保生成符合阿里云要求的RGBA四通道PNG图像
*
* @param imageUrl 原始图片URL
* @return 转换后的图片字节数组
*/
public byte[] convertImageToAliyunFormatAdvanced(String imageUrl) {
try {
log.info("开始高级图片转换为阿里云格式");
// 1. 下载原始图片
BufferedImage originalImage = downloadImage(imageUrl);
if (originalImage == null) {
log.error("无法下载原始图片");
return null;
}
int width = originalImage.getWidth();
int height = originalImage.getHeight();
log.info("原始图片尺寸: {}x{}, 类型: {}", width, height, getImageTypeName(originalImage.getType()));
// 2. 检查尺寸如果超过2048则缩放
if (Math.max(width, height) > 2048) {
double scale = 2048.0 / Math.max(width, height);
int newWidth = (int) (width * scale);
int newHeight = (int) (height * scale);
log.info("图片尺寸过大,缩放至: {}x{}", newWidth, newHeight);
BufferedImage scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = scaledImage.createGraphics();
// 设置最高质量渲染
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// 绘制原始图片到缩放图片上
g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
g2d.dispose();
originalImage = scaledImage;
width = newWidth;
height = newHeight;
}
// 3. 创建真正的RGBA四通道图像强制透明背景
BufferedImage rgbaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = rgbaImage.createGraphics();
// 设置最高质量渲染
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// 强制清空背景为完全透明
g2d.setComposite(AlphaComposite.Clear);
g2d.fillRect(0, 0, width, height);
// 设置正常合成模式
g2d.setComposite(AlphaComposite.SrcOver);
// 如果原始图片没有透明背景,使用智能背景移除
if (!originalImage.getColorModel().hasAlpha() || !hasTransparentPixels(originalImage)) {
log.info("原始图片没有透明背景,使用智能背景移除");
// 使用智能背景移除算法
BufferedImage transparentImage = createTransparentBackgroundImage(originalImage);
// 将处理后的图片绘制到RGBA图像上
g2d.drawImage(transparentImage, 0, 0, null);
// 验证透明像素数量
int transparentCount = 0;
for (int y = 0; y < height; y += Math.max(1, height / 50)) {
for (int x = 0; x < width; x += Math.max(1, width / 50)) {
int rgb = rgbaImage.getRGB(x, y);
int alpha = (rgb >> 24) & 0xFF;
if (alpha == 0) {
transparentCount++;
}
}
}
log.info("智能背景移除完成,采样透明像素比例: {:.2f}%",
(double) transparentCount / ((width / Math.max(1, width / 50)) * (height / Math.max(1, height / 50))) * 100);
} else {
// 原始图片已有透明度,直接绘制
g2d.drawImage(originalImage, 0, 0, null);
}
g2d.dispose();
// 4. 深度验证RGBA图像
boolean hasTransparency = rgbaImage.getColorModel().hasAlpha();
log.info("RGBA图像是否支持透明度: {}", hasTransparency);
// 详细分析透明度
analyzeTransparency(rgbaImage);
analyzeColorMode(rgbaImage);
// 5. 转换为PNG格式字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean success = ImageIO.write(rgbaImage, "PNG", baos);
if (success) {
byte[] imageBytes = baos.toByteArray();
log.info("高级图片转换成功,大小: {} bytes", imageBytes.length);
// 验证PNG文件头
if (imageBytes.length >= 8) {
boolean isPng = (imageBytes[0] == (byte) 0x89 && imageBytes[1] == 0x50 &&
imageBytes[2] == 0x4E && imageBytes[3] == 0x47);
log.info("输出文件是否为PNG格式: {}", isPng);
// 检查PNG是否包含透明度信息
if (imageBytes.length > 25) {
// 检查IHDR chunk中的颜色类型
boolean hasAlphaChannel = false;
for (int i = 0; i < imageBytes.length - 25; i++) {
if (imageBytes[i] == 'I' && imageBytes[i+1] == 'H' &&
imageBytes[i+2] == 'D' && imageBytes[i+3] == 'R') {
// 颜色类型在第25个字节
int colorType = imageBytes[i + 25] & 0xFF;
hasAlphaChannel = (colorType == 4 || colorType == 6);
log.info("PNG颜色类型: {}, 是否包含Alpha通道: {}", colorType, hasAlphaChannel);
break;
}
}
}
}
return imageBytes;
} else {
log.error("高级图片转换失败");
return null;
}
} catch (Exception e) {
log.error("高级图片转换失败", e);
return null;
}
}
/**
* 下载图片
*
* @param imageUrl 图片URL
* @return BufferedImage对象
*/
private BufferedImage downloadImage(String imageUrl) {
try {
URL url = new URL(imageUrl);
return ImageIO.read(url);
} catch (Exception e) {
log.error("下载图片失败: {}", imageUrl, e);
return null;
}
}
/**
* 获取图片类型名称
*
* @param type 图片类型
* @return 类型名称
*/
private String getImageTypeName(int type) {
switch (type) {
case BufferedImage.TYPE_INT_RGB:
return "RGB";
case BufferedImage.TYPE_INT_ARGB:
return "ARGB";
case BufferedImage.TYPE_INT_ARGB_PRE:
return "ARGB_PRE";
case BufferedImage.TYPE_4BYTE_ABGR:
return "ABGR";
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
return "ABGR_PRE";
case BufferedImage.TYPE_3BYTE_BGR:
return "BGR";
case BufferedImage.TYPE_BYTE_GRAY:
return "GRAY";
case BufferedImage.TYPE_USHORT_555_RGB:
return "RGB_555";
case BufferedImage.TYPE_USHORT_565_RGB:
return "RGB_565";
default:
return "UNKNOWN";
}
}
/**
* 分析透明度
*
* @param image 图片对象
*/
private void analyzeTransparency(BufferedImage image) {
try {
int width = image.getWidth();
int height = image.getHeight();
boolean hasTransparency = image.getColorModel().hasAlpha();
log.info("是否支持透明度: {}", hasTransparency);
// 检查是否有透明像素
boolean hasTransparentPixels = false;
for (int y = 0; y < height; y += 10) { // 采样检查
for (int x = 0; x < width; x += 10) {
int rgb = image.getRGB(x, y);
int alpha = (rgb >> 24) & 0xFF;
if (alpha < 255) {
hasTransparentPixels = true;
break;
}
}
if (hasTransparentPixels) break;
}
log.info("是否包含透明像素: {}", hasTransparentPixels);
} catch (Exception e) {
log.error("分析透明度失败", e);
}
}
/**
* 分析颜色模式
*
* @param image 图片对象
*/
private void analyzeColorMode(BufferedImage image) {
try {
int width = image.getWidth();
int height = image.getHeight();
// 采样分析颜色
int sampleSize = Math.min(100, Math.min(width, height));
int stepX = width / sampleSize;
int stepY = height / sampleSize;
int transparentPixels = 0;
int opaquePixels = 0;
int totalPixels = 0;
for (int y = 0; y < height; y += stepY) {
for (int x = 0; x < width; x += stepX) {
int rgb = image.getRGB(x, y);
int alpha = (rgb >> 24) & 0xFF;
if (alpha == 0) {
transparentPixels++;
} else if (alpha == 255) {
opaquePixels++;
}
totalPixels++;
}
}
double transparentRatio = (double) transparentPixels / totalPixels;
double opaqueRatio = (double) opaquePixels / totalPixels;
log.info("透明度分析:");
log.info(" 透明像素比例: {:.2f}%", transparentRatio * 100);
log.info(" 不透明像素比例: {:.2f}%", opaqueRatio * 100);
log.info(" 半透明像素比例: {:.2f}%", (1 - transparentRatio - opaqueRatio) * 100);
} catch (Exception e) {
log.error("分析颜色模式失败", e);
}
}
/**
* 检查图片是否包含透明像素
*
* @param image 图片对象
* @return 是否包含透明像素
*/
private boolean hasTransparentPixels(BufferedImage image) {
if (!image.getColorModel().hasAlpha()) {
return false;
}
int width = image.getWidth();
int height = image.getHeight();
int sampleSize = Math.min(1000, width * height);
for (int i = 0; i < sampleSize; i++) {
int x = (i * width) / sampleSize;
int y = (i * height) / sampleSize;
int rgb = image.getRGB(x, y);
int alpha = (rgb >> 24) & 0xFF;
if (alpha < 255) {
return true;
}
}
return false;
}
/**
* 创建带透明背景的图片(专门处理无透明背景的图片)
* 使用高级背景移除算法,确保彻底移除背景
*
* @param originalImage 原始图片
* @return 带透明背景的图片
*/
private BufferedImage createTransparentBackgroundImage(BufferedImage originalImage) {
int width = originalImage.getWidth();
int height = originalImage.getHeight();
log.info("开始高级背景移除处理,图片尺寸: {}x{}", width, height);
// 创建RGBA图像
BufferedImage transparentImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// 第一步:分析图片,找到主体区域
int[] edgePixels = findEdgePixels(originalImage);
int leftEdge = edgePixels[0];
int rightEdge = edgePixels[1];
int topEdge = edgePixels[2];
int bottomEdge = edgePixels[3];
log.info("检测到主体区域: 左={}, 右={}, 上={}, 下={}", leftEdge, rightEdge, topEdge, bottomEdge);
// 第二步:分析背景颜色特征
Color[] backgroundColors = analyzeBackgroundColors(originalImage, edgePixels);
log.info("分析到背景颜色数量: {}", backgroundColors.length);
// 第三步:高级背景移除处理
int transparentCount = 0;
int subjectCount = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = originalImage.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
// 多重判断是否为主体像素
boolean isSubject = isSubjectPixel(x, y, r, g, b, edgePixels, backgroundColors, originalImage);
if (isSubject) {
// 主体区域:保持原色,但可能进行边缘羽化
int alpha = calculateEdgeAlpha(x, y, edgePixels, width, height);
transparentImage.setRGB(x, y, (alpha << 24) | (rgb & 0xFFFFFF));
subjectCount++;
} else {
// 背景区域:设为透明
transparentImage.setRGB(x, y, 0x00000000);
transparentCount++;
}
}
}
log.info("背景移除完成 - 主体像素: {}, 透明像素: {}, 主体比例: {:.2f}%",
subjectCount, transparentCount, (double) subjectCount / (width * height) * 100);
// 第四步:后处理优化
BufferedImage optimizedImage = postProcessTransparentImage(transparentImage, originalImage);
return optimizedImage;
}
/**
* 分析背景颜色特征
*
* @param image 图片对象
* @param edgePixels 主体边缘坐标
* @return 背景颜色数组
*/
private Color[] analyzeBackgroundColors(BufferedImage image, int[] edgePixels) {
int width = image.getWidth();
int height = image.getHeight();
int leftEdge = edgePixels[0];
int rightEdge = edgePixels[1];
int topEdge = edgePixels[2];
int bottomEdge = edgePixels[3];
List<Color> backgroundColors = new ArrayList<>();
// 采样边缘区域的背景颜色
int sampleSize = 100;
for (int i = 0; i < sampleSize; i++) {
int x, y;
// 随机选择边缘区域外的像素
if (i % 4 == 0) {
// 左侧边缘
x = (int) (Math.random() * Math.max(1, leftEdge));
y = (int) (Math.random() * height);
} else if (i % 4 == 1) {
// 右侧边缘
x = rightEdge + (int) (Math.random() * Math.max(1, width - rightEdge));
y = (int) (Math.random() * height);
} else if (i % 4 == 2) {
// 上侧边缘
x = (int) (Math.random() * width);
y = (int) (Math.random() * Math.max(1, topEdge));
} else {
// 下侧边缘
x = (int) (Math.random() * width);
y = bottomEdge + (int) (Math.random() * Math.max(1, height - bottomEdge));
}
if (x >= 0 && x < width && y >= 0 && y < height) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
backgroundColors.add(new Color(r, g, b));
}
}
return backgroundColors.toArray(new Color[0]);
}
}