From 97b1d4784217bb81e592f08d2744c72ae16d3712 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:45:56 +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/service/ImageAnalysisService.java | 263 +++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java diff --git a/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java b/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java new file mode 100644 index 0000000..1681cc6 --- /dev/null +++ b/src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java @@ -0,0 +1,263 @@ +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); + } +}