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.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 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]); } /** * 判断像素是否为主体像素(多重条件判断) * * @param x 像素X坐标 * @param y 像素Y坐标 * @param r 红色值 * @param g 绿色值 * @param b 蓝色值 * @param edgePixels 主体边缘坐标 * @param backgroundColors 背景颜色数组 * @param image 原始图片 * @return 是否为主体像素 */ private boolean isSubjectPixel(int x, int y, int r, int g, int b, int[] edgePixels, Color[] backgroundColors, BufferedImage image) { int leftEdge = edgePixels[0]; int rightEdge = edgePixels[1]; int topEdge = edgePixels[2]; int bottomEdge = edgePixels[3]; // 条件1:位置判断 - 必须在主体区域内 boolean inSubjectArea = (x >= leftEdge && x <= rightEdge && y >= topEdge && y <= bottomEdge); if (!inSubjectArea) { return false; } // 条件2:颜色判断 - 不能是明显的背景色 boolean isBackgroundColor = false; for (Color bgColor : backgroundColors) { int colorDiff = Math.abs(r - bgColor.getRed()) + Math.abs(g - bgColor.getGreen()) + Math.abs(b - bgColor.getBlue()); if (colorDiff < 30) { // 颜色相似度阈值 isBackgroundColor = true; break; } } // 条件3:亮度判断 - 不能是过亮或过暗的背景色 int brightness = (r + g + b) / 3; boolean isExtremeBrightness = brightness > 240 || brightness < 20; // 条件4:颜色均匀性判断 - 不能是过于均匀的颜色 boolean isUniformColor = Math.abs(r - g) < 10 && Math.abs(g - b) < 10 && Math.abs(r - b) < 10; // 条件5:边缘检测 - 检查周围像素的变化 boolean hasColorVariation = hasSignificantColorVariation(image, x, y); // 综合判断:必须满足主体区域条件,且不满足背景特征 return inSubjectArea && !isBackgroundColor && !isExtremeBrightness && (!isUniformColor || hasColorVariation); } /** * 检查像素周围是否有显著的颜色变化 * * @param image 图片对象 * @param x 像素X坐标 * @param y 像素Y坐标 * @return 是否有显著颜色变化 */ private boolean hasSignificantColorVariation(BufferedImage image, int x, int y) { int width = image.getWidth(); int height = image.getHeight(); int centerRgb = image.getRGB(x, y); int centerR = (centerRgb >> 16) & 0xFF; int centerG = (centerRgb >> 8) & 0xFF; int centerB = centerRgb & 0xFF; int variationCount = 0; int totalSamples = 0; // 检查周围8个像素 for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx == 0 && dy == 0) continue; int nx = x + dx; int ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { int neighborRgb = image.getRGB(nx, ny); int neighborR = (neighborRgb >> 16) & 0xFF; int neighborG = (neighborRgb >> 8) & 0xFF; int neighborB = neighborRgb & 0xFF; int colorDiff = Math.abs(centerR - neighborR) + Math.abs(centerG - neighborG) + Math.abs(centerB - neighborB); if (colorDiff > 30) { // 颜色差异阈值 variationCount++; } totalSamples++; } } } return totalSamples > 0 && (double) variationCount / totalSamples > 0.3; } /** * 计算边缘像素的Alpha值(羽化效果) * * @param x 像素X坐标 * @param y 像素Y坐标 * @param edgePixels 主体边缘坐标 * @param width 图片宽度 * @param height 图片高度 * @return Alpha值 */ private int calculateEdgeAlpha(int x, int y, int[] edgePixels, int width, int height) { int leftEdge = edgePixels[0]; int rightEdge = edgePixels[1]; int topEdge = edgePixels[2]; int bottomEdge = edgePixels[3]; // 计算到边缘的距离 int distToLeft = x - leftEdge; int distToRight = rightEdge - x; int distToTop = y - topEdge; int distToBottom = bottomEdge - y; int minDist = Math.min(Math.min(distToLeft, distToRight), Math.min(distToTop, distToBottom)); // 如果距离边缘很近,进行羽化处理 if (minDist <= 3) { return Math.max(128, 255 - (3 - minDist) * 40); // 边缘羽化 } return 255; // 完全不透明 } /** * 后处理透明图片,进一步优化背景移除效果 * * @param transparentImage 初步处理的透明图片 * @param originalImage 原始图片 * @return 优化后的透明图片 */ private BufferedImage postProcessTransparentImage(BufferedImage transparentImage, BufferedImage originalImage) { int width = transparentImage.getWidth(); int height = transparentImage.getHeight(); log.info("开始后处理优化,进一步清理残留背景"); BufferedImage optimizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // 复制透明图片 Graphics2D g2d = optimizedImage.createGraphics(); g2d.drawImage(transparentImage, 0, 0, null); g2d.dispose(); // 多轮清理残留背景 for (int round = 1; round <= 3; round++) { log.info("执行第{}轮背景清理", round); int cleanedPixels = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb = optimizedImage.getRGB(x, y); int alpha = (rgb >> 24) & 0xFF; // 只处理非透明像素 if (alpha > 0) { int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; // 检查是否应该被清理 if (shouldCleanPixel(x, y, r, g, b, optimizedImage, round)) { optimizedImage.setRGB(x, y, 0x00000000); // 设为透明 cleanedPixels++; } } } } log.info("第{}轮清理完成,清理了{}个像素", round, cleanedPixels); // 如果清理的像素很少,说明已经比较干净了 if (cleanedPixels < 100) { break; } } // 最终验证透明像素比例 int finalTransparentCount = 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 = optimizedImage.getRGB(x, y); int alpha = (rgb >> 24) & 0xFF; if (alpha == 0) { finalTransparentCount++; } } } log.info("后处理完成,最终透明像素比例: {:.2f}%", (double) finalTransparentCount / ((width / Math.max(1, width / 50)) * (height / Math.max(1, height / 50))) * 100); return optimizedImage; } /** * 判断像素是否应该被清理 * * @param x 像素X坐标 * @param y 像素Y坐标 * @param r 红色值 * @param g 绿色值 * @param b 蓝色值 * @param image 当前图片 * @param round 清理轮次 * @return 是否应该清理 */ private boolean shouldCleanPixel(int x, int y, int r, int g, int b, BufferedImage image, int round) { int width = image.getWidth(); int height = image.getHeight(); // 检查周围透明像素的比例 int transparentNeighbors = 0; int totalNeighbors = 0; for (int dy = -2; dy <= 2; dy++) { for (int dx = -2; dx <= 2; dx++) { if (dx == 0 && dy == 0) continue; int nx = x + dx; int ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { int neighborRgb = image.getRGB(nx, ny); int neighborAlpha = (neighborRgb >> 24) & 0xFF; if (neighborAlpha == 0) { transparentNeighbors++; } totalNeighbors++; } } } double transparentRatio = totalNeighbors > 0 ? (double) transparentNeighbors / totalNeighbors : 0; // 根据轮次调整清理策略 double threshold = 0.6 - (round - 1) * 0.1; // 逐渐降低阈值 // 如果周围透明像素比例很高,且当前像素颜色特征像背景,则清理 if (transparentRatio > threshold) { // 检查颜色特征 int brightness = (r + g + b) / 3; boolean isUniformColor = Math.abs(r - g) < 15 && Math.abs(g - b) < 15 && Math.abs(r - b) < 15; boolean isExtremeBrightness = brightness > 220 || brightness < 30; return isUniformColor || isExtremeBrightness; } return false; } /** * 找到图片的主体边缘 * * @param image 图片对象 * @return [left, right, top, bottom] 边缘坐标 */ private int[] findEdgePixels(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); int leftEdge = width; int rightEdge = 0; int topEdge = height; int bottomEdge = 0; // 扫描图片找到非背景像素的边界 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb = image.getRGB(x, y); int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; // 判断是否为主体像素(非白色/浅色背景) boolean isSubject = !(r > 240 && g > 240 && b > 240); if (isSubject) { if (x < leftEdge) leftEdge = x; if (x > rightEdge) rightEdge = x; if (y < topEdge) topEdge = y; if (y > bottomEdge) bottomEdge = y; } } } // 确保边界有效 leftEdge = Math.max(0, leftEdge - 5); // 留一些边距 rightEdge = Math.min(width - 1, rightEdge + 5); topEdge = Math.max(0, topEdge - 5); bottomEdge = Math.min(height - 1, bottomEdge + 5); return new int[]{leftEdge, rightEdge, topEdge, bottomEdge}; } }