From cbe63190cb435461028d48c69f4133f083f62cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 26 Jul 2025 17:48:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=9B=BE=E5=83=8F=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/common/utils/ImageAnalysisUtil.java | 258 ++++++++++++++++++++ .../app/service/ImageAnalysisService.java | 263 --------------------- 2 files changed, 258 insertions(+), 263 deletions(-) create mode 100644 src/main/java/com/iflytop/colortitration/app/common/utils/ImageAnalysisUtil.java delete mode 100644 src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java diff --git a/src/main/java/com/iflytop/colortitration/app/common/utils/ImageAnalysisUtil.java b/src/main/java/com/iflytop/colortitration/app/common/utils/ImageAnalysisUtil.java new file mode 100644 index 0000000..409827a --- /dev/null +++ b/src/main/java/com/iflytop/colortitration/app/common/utils/ImageAnalysisUtil.java @@ -0,0 +1,258 @@ +package com.iflytop.colortitration.app.common.utils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ConvolveOp; +import java.awt.image.Kernel; +import java.io.File; +import java.io.IOException; + +/** + * 工具类:图像分析,可扩展多种图像处理与分析方法,方法均为静态调用 + */ +public class ImageAnalysisUtil { + /** + * RGB 空间最大距离 + */ + private static final double MAX_DISTANCE = Math.sqrt(3 * 255 * 255); + /** + * 泡沫过滤:亮度与均值偏差倍数 + */ + private static final double BUBBLE_STD_MULTIPLIER = 1.5; + /** + * 默认高斯平滑内核(3x3) + */ + private static final float[] GAUSS_KERNEL = { + 1 / 16f, 2 / 16f, 1 / 16f, + 2 / 16f, 4 / 16f, 2 / 16f, + 1 / 16f, 2 / 16f, 1 / 16f + }; + + // 私有构造,避免实例化 + private ImageAnalysisUtil() { + } + + /** + * 使用百分比容差(0–100%)比较图像文件指定区域的平均颜色与目标颜色是否在容差范围内。 + * + * @param imageFile 待分析的图像文件 + * @param targetColor 目标颜色 + * @param tolerancePercent 容差百分比(0–100),表示允许的最大色差百分比 + * @param region 指定区域(x, y, width, height) + * @return 如果区域平均颜色与目标颜色距离 <= 百分比转换后的绝对距离,则返回 true;否则返回 false + * @throws IOException 如果读取图像文件失败 + */ + public static boolean isRegionColorWithinTolerance(File imageFile, Color targetColor, double tolerancePercent, Rectangle region) throws IOException { + BufferedImage image = loadImage(imageFile); + Rectangle validRegion = getValidRegion(image, region); + + long sumR = 0, sumG = 0, sumB = 0; + int count = 0; + for (int x = validRegion.x; x < validRegion.x + validRegion.width; x++) { + for (int y = validRegion.y; y < validRegion.y + validRegion.height; y++) { + Color c = new Color(image.getRGB(x, y), true); + sumR += c.getRed(); + sumG += c.getGreen(); + sumB += c.getBlue(); + count++; + } + } + Color avgColor = new Color( + (int) (sumR / count), + (int) (sumG / count), + (int) (sumB / count) + ); + double distance = computeDistance(avgColor, targetColor); + return distance <= convertTolerance(tolerancePercent); + } + + /** + * 使用图像处理(高斯平滑 + 泡沫过滤)和百分比容差(0–100%) + * 分析图像文件指定区域的平均颜色是否与目标颜色匹配。 + * + * @param imageFile 待分析的图像文件 + * @param targetColor 目标颜色 + * @param tolerancePercent 容差百分比(0–100),表示允许的最大色差百分比 + * @param region 指定区域(x, y, width, height) + * @return 如果区域平均颜色与目标颜色距离 <= 百分比转换后的绝对距离,则返回 true;否则返回 false + * @throws IOException 如果读取图像文件失败 + */ + public static boolean analyzeRegionColorMatch(File imageFile, Color targetColor, double tolerancePercent, Rectangle region) throws IOException { + BufferedImage image = loadImage(imageFile); + BufferedImage smoothed = smoothImage(image); + Rectangle validRegion = getValidRegion(smoothed, region); + + int width = validRegion.width; + int height = validRegion.height; + int area = width * height; + double[] brightness = new double[area]; + int idx = 0; + for (int x = validRegion.x; x < validRegion.x + width; x++) { + for (int y = validRegion.y; y < validRegion.y + height; y++) { + Color c = new Color(smoothed.getRGB(x, y), true); + brightness[idx++] = (c.getRed() + c.getGreen() + c.getBlue()) / 3.0; + } + } + double sum = 0; + for (double v : brightness) sum += v; + double mean = sum / area; + double var = 0; + for (double v : brightness) var += Math.pow(v - mean, 2); + double std = Math.sqrt(var / area); + + long sumR = 0, sumG = 0, sumB = 0; + int validCount = 0; + idx = 0; + for (int x = validRegion.x; x < validRegion.x + width; x++) { + for (int y = validRegion.y; y < validRegion.y + height; y++) { + if (Math.abs(brightness[idx++] - mean) <= BUBBLE_STD_MULTIPLIER * std) { + Color c = new Color(smoothed.getRGB(x, y), true); + sumR += c.getRed(); + sumG += c.getGreen(); + sumB += c.getBlue(); + validCount++; + } + } + } + Color avgColor = validCount == 0 + ? new Color(0, 0, 0) + : new Color( + (int) (sumR / validCount), + (int) (sumG / validCount), + (int) (sumB / validCount) + ); + double distance = computeDistance(avgColor, targetColor); + return distance <= convertTolerance(tolerancePercent); + } + + /** + * 基于背景差分判断图像中是否存在试管 + * + * @param backgroundFile 空场景图像文件 + * @param currentFile 当前图像文件 + * @param region 区域(x, y, width, height) + * @param diffThresholdPercent 差分阈值百分比(0–100),灰度差分阈值 = 255 * 百分比 + * @param minArea 最小像素面积阈值 + * @param minAspectRatio 最小高宽比 (height/width) + * @return 如果检测到符合特征的区域,返回 true;否则返回 false + * @throws IOException 读取图像失败 + */ + public static boolean isTestTubePresent(File backgroundFile, File currentFile, Rectangle region, double diffThresholdPercent, int minArea, double minAspectRatio) throws IOException { + BufferedImage bg = smoothImage(loadImage(backgroundFile)); + BufferedImage curr = smoothImage(loadImage(currentFile)); + + Rectangle validBg = getValidRegion(bg, region); + Rectangle validCurr = getValidRegion(curr, region); + Rectangle validRegion = validBg.intersection(validCurr); + if (validRegion.isEmpty()) { + throw new IllegalArgumentException("指定区域超出图像范围: " + region); + } + + int width = validRegion.width; + int height = validRegion.height; + double threshold = 255 * (diffThresholdPercent / 100.0); + + int countMask = 0; + int minX = width, minY = height; + int maxX = 0, maxY = 0; + for (int dx = 0; dx < width; dx++) { + for (int dy = 0; dy < height; dy++) { + int x = validRegion.x + dx; + int y = validRegion.y + dy; + Color cb = new Color(bg.getRGB(x, y), true); + Color cc = new Color(curr.getRGB(x, y), true); + double bBg = (cb.getRed() + cb.getGreen() + cb.getBlue()) / 3.0; + double bCurr = (cc.getRed() + cc.getGreen() + cc.getBlue()) / 3.0; + if (Math.abs(bBg - bCurr) >= threshold) { + countMask++; + minX = Math.min(minX, dx); + minY = Math.min(minY, dy); + maxX = Math.max(maxX, dx); + maxY = Math.max(maxY, dy); + } + } + } + if (countMask < minArea) { + return false; + } + int boxW = maxX - minX + 1; + int boxH = maxY - minY + 1; + double aspect = (double) boxH / boxW; + return aspect >= minAspectRatio; + } + + // --------------- 私有辅助方法 --------------- + + /** + * 读取图像文件并返回 BufferedImage 对象 + * + * @param imageFile 图像文件 + * @return BufferedImage 图像对象 + * @throws IOException 如果读取失败抛出异常 + */ + private static BufferedImage loadImage(File imageFile) throws IOException { + BufferedImage img = ImageIO.read(imageFile); + if (img == null) { + throw new IOException("无法从文件读取图像: " + imageFile); + } + return img; + } + + /** + * 校验并返回图像与指定区域的交集区域 + * + * @param image 图像对象 + * @param region 指定区域 + * @return Rectangle 有效区域 + * @throws IllegalArgumentException 如果区域超出图像范围 + */ + private static Rectangle getValidRegion(BufferedImage image, Rectangle region) { + Rectangle bounds = new Rectangle(0, 0, image.getWidth(), image.getHeight()); + Rectangle valid = bounds.intersection(region); + if (valid.isEmpty()) { + throw new IllegalArgumentException("指定区域超出图像范围: " + region); + } + return valid; + } + + /** + * 对图像执行 3x3 高斯平滑操作,减少噪声 + * + * @param src 源图像 + * @return BufferedImage 平滑后的图像 + */ + private static BufferedImage smoothImage(BufferedImage src) { + Kernel kernel = new Kernel(3, 3, GAUSS_KERNEL); + return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null).filter(src, null); + } + + /** + * 计算两种颜色在 RGB 空间的欧氏距离 + * + * @param c1 颜色一 + * @param c2 颜色二 + * @return double 欧氏距离 + */ + private static double computeDistance(Color c1, Color c2) { + return Math.sqrt( + Math.pow(c1.getRed() - c2.getRed(), 2) + + Math.pow(c1.getGreen() - c2.getGreen(), 2) + + Math.pow(c1.getBlue() - c2.getBlue(), 2) + ); + } + + /** + * 将百分比容差转换为在 RGB 空间中的绝对距离阈值 + * + * @param percent 容差百分比(0–100) + * @return double 绝对距离阈值 + * @throws IllegalArgumentException 如果百分比不在 0–100 范围内 + */ + private static double convertTolerance(double percent) { + if (percent < 0 || percent > 100) { + throw new IllegalArgumentException("tolerancePercent 范围应在 0–100 之间"); + } + return MAX_DISTANCE * (percent / 100.0); + } +} diff --git a/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java b/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java deleted file mode 100644 index 1681cc6..0000000 --- a/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java +++ /dev/null @@ -1,263 +0,0 @@ -package com.iflytop.colortitration.app.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.ConvolveOp; -import java.awt.image.Kernel; -import java.io.File; -import java.io.IOException; - -/** - * 图像分析 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ImageAnalysisService { - /** - * RGB 空间最大距离 - */ - private static final double MAX_DISTANCE = Math.sqrt(3 * 255 * 255); - - /** - * 泡沫过滤:亮度与均值偏差倍数 - */ - private static final double BUBBLE_STD_MULTIPLIER = 1.5; - - /** - * 默认高斯平滑内核(3x3) - */ - private static final float[] GAUSS_KERNEL = { - 1 / 16f, 2 / 16f, 1 / 16f, - 2 / 16f, 4 / 16f, 2 / 16f, - 1 / 16f, 2 / 16f, 1 / 16f - }; - - /** - * 使用百分比容差(0–100%)比较图像文件指定区域的平均颜色与目标颜色是否在容差范围内。 - * - * @param imageFile 待分析的图像文件 - * @param targetColor 目标颜色 - * @param tolerancePercent 容差百分比(0–100),表示允许的最大色差百分比 - * @param region 指定区域(x, y, width, height) - * @return 如果区域平均颜色与目标颜色距离 <= 百分比转换后的绝对距离,则返回 true;否则返回 false - * @throws IOException 如果读取图像文件失败 - */ - public boolean isRegionColorWithinTolerance(File imageFile, Color targetColor, double tolerancePercent, Rectangle region) throws IOException { - BufferedImage image = loadImage(imageFile); - Rectangle validRegion = getValidRegion(image, region); - - long sumR = 0, sumG = 0, sumB = 0; - int count = 0; - for (int x = validRegion.x; x < validRegion.x + validRegion.width; x++) { - for (int y = validRegion.y; y < validRegion.y + validRegion.height; y++) { - Color c = new Color(image.getRGB(x, y), true); - sumR += c.getRed(); - sumG += c.getGreen(); - sumB += c.getBlue(); - count++; - } - } - Color avgColor = new Color( - (int) (sumR / count), - (int) (sumG / count), - (int) (sumB / count) - ); - double distance = computeDistance(avgColor, targetColor); - return distance <= convertTolerance(tolerancePercent); - } - - /** - * 使用图像处理(高斯平滑 + 泡沫过滤)和百分比容差(0–100%) - * 分析图像文件指定区域的平均颜色是否与目标颜色匹配。 - * - * @param imageFile 待分析的图像文件 - * @param targetColor 目标颜色 - * @param tolerancePercent 容差百分比(0–100),表示允许的最大色差百分比 - * @param region 指定区域(x, y, width, height) - * @return 如果区域平均颜色与目标颜色距离 <= 百分比转换后的绝对距离,则返回 true;否则返回 false - * @throws IOException 如果读取图像文件失败 - */ - public boolean analyzeRegionColorMatch(File imageFile, Color targetColor, double tolerancePercent, Rectangle region) throws IOException { - BufferedImage image = loadImage(imageFile); - BufferedImage smoothed = smoothImage(image); - Rectangle validRegion = getValidRegion(smoothed, region); - - int width = validRegion.width; - int height = validRegion.height; - int area = width * height; - double[] brightness = new double[area]; - int idx = 0; - for (int x = validRegion.x; x < validRegion.x + width; x++) { - for (int y = validRegion.y; y < validRegion.y + height; y++) { - Color c = new Color(smoothed.getRGB(x, y), true); - brightness[idx++] = (c.getRed() + c.getGreen() + c.getBlue()) / 3.0; - } - } - double sum = 0; - for (double v : brightness) sum += v; - double mean = sum / area; - double var = 0; - for (double v : brightness) var += Math.pow(v - mean, 2); - double std = Math.sqrt(var / area); - - long sumR = 0, sumG = 0, sumB = 0; - int validCount = 0; - idx = 0; - for (int x = validRegion.x; x < validRegion.x + width; x++) { - for (int y = validRegion.y; y < validRegion.y + height; y++) { - if (Math.abs(brightness[idx++] - mean) <= BUBBLE_STD_MULTIPLIER * std) { - Color c = new Color(smoothed.getRGB(x, y), true); - sumR += c.getRed(); - sumG += c.getGreen(); - sumB += c.getBlue(); - validCount++; - } - } - } - Color avgColor = validCount == 0 - ? new Color(0, 0, 0) - : new Color( - (int) (sumR / validCount), - (int) (sumG / validCount), - (int) (sumB / validCount) - ); - double distance = computeDistance(avgColor, targetColor); - return distance <= convertTolerance(tolerancePercent); - } - - /** - * 基于背景差分判断图像中是否存在试管 - * - * @param backgroundFile 空场景图像文件 - * @param currentFile 当前图像文件 - * @param region 区域(x, y, width, height) - * @param diffThresholdPercent 差分阈值百分比(0–100),灰度差分阈值 = 255 * 百分比 - * @param minArea 最小像素面积阈值 - * @param minAspectRatio 最小高宽比 (height/width) - * @return 如果检测到符合特征的区域,返回 true;否则返回 false - * @throws IOException 读取图像失败 - */ - public boolean isTestTubePresent(File backgroundFile, File currentFile, Rectangle region, double diffThresholdPercent, int minArea, double minAspectRatio) throws IOException { - BufferedImage bg = smoothImage(loadImage(backgroundFile)); - BufferedImage curr = smoothImage(loadImage(currentFile)); - - Rectangle validBg = getValidRegion(bg, region); - Rectangle validCurr = getValidRegion(curr, region); - Rectangle validRegion = validBg.intersection(validCurr); - if (validRegion.isEmpty()) { - throw new IllegalArgumentException("指定区域超出图像范围: " + region); - } - - int width = validRegion.width; - int height = validRegion.height; - double threshold = 255 * (diffThresholdPercent / 100.0); - - int countMask = 0; - int minX = width, minY = height; - int maxX = 0, maxY = 0; - for (int dx = 0; dx < width; dx++) { - for (int dy = 0; dy < height; dy++) { - int x = validRegion.x + dx; - int y = validRegion.y + dy; - Color cb = new Color(bg.getRGB(x, y), true); - Color cc = new Color(curr.getRGB(x, y), true); - double bBg = (cb.getRed() + cb.getGreen() + cb.getBlue()) / 3.0; - double bCurr = (cc.getRed() + cc.getGreen() + cc.getBlue()) / 3.0; - if (Math.abs(bBg - bCurr) >= threshold) { - countMask++; - minX = Math.min(minX, dx); - minY = Math.min(minY, dy); - maxX = Math.max(maxX, dx); - maxY = Math.max(maxY, dy); - } - } - } - if (countMask < minArea) { - return false; - } - int boxW = maxX - minX + 1; - int boxH = maxY - minY + 1; - double aspect = (double) boxH / boxW; - return aspect >= minAspectRatio; - } - - // --------------- 私有 --------------- - - /** - * 读取图像文件并返回 BufferedImage 对象 - * - * @param imageFile 图像文件 - * @return BufferedImage 图像对象 - * @throws IOException 如果读取失败抛出异常 - */ - private BufferedImage loadImage(File imageFile) throws IOException { - BufferedImage img = ImageIO.read(imageFile); - if (img == null) { - throw new IOException("无法从文件读取图像: " + imageFile); - } - return img; - } - - /** - * 校验并返回图像与指定区域的交集区域 - * - * @param image 图像对象 - * @param region 指定区域 - * @return Rectangle 有效区域 - * @throws IllegalArgumentException 如果区域超出图像范围 - */ - private Rectangle getValidRegion(BufferedImage image, Rectangle region) { - Rectangle bounds = new Rectangle(0, 0, image.getWidth(), image.getHeight()); - Rectangle valid = bounds.intersection(region); - if (valid.isEmpty()) { - throw new IllegalArgumentException("指定区域超出图像范围: " + region); - } - return valid; - } - - /** - * 对图像执行 3x3 高斯平滑操作,减少噪声 - * - * @param src 源图像 - * @return BufferedImage 平滑后的图像 - */ - private BufferedImage smoothImage(BufferedImage src) { - Kernel kernel = new Kernel(3, 3, GAUSS_KERNEL); - return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null).filter(src, null); - } - - /** - * 计算两种颜色在 RGB 空间的欧氏距离 - * - * @param c1 颜色一 - * @param c2 颜色二 - * @return double 欧氏距离 - */ - private double computeDistance(Color c1, Color c2) { - return Math.sqrt( - Math.pow(c1.getRed() - c2.getRed(), 2) + - Math.pow(c1.getGreen() - c2.getGreen(), 2) + - Math.pow(c1.getBlue() - c2.getBlue(), 2) - ); - } - - /** - * 将百分比容差转换为在 RGB 空间中的绝对距离阈值 - * - * @param percent 容差百分比(0–100) - * @return double 绝对距离阈值 - * @throws IllegalArgumentException 如果百分比不在 0–100 范围内 - */ - private double convertTolerance(double percent) { - if (percent < 0 || percent > 100) { - throw new IllegalArgumentException("tolerancePercent 范围应在 0–100 之间"); - } - return MAX_DISTANCE * (percent / 100.0); - } -}