Browse Source

feat:图像分析

master
白凤吉 1 week ago
parent
commit
97b1d47842
  1. 263
      src/main/java/com/iflytop/colortitration/app/service/ImageAnalysisService.java

263
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
};
/**
* 使用百分比容差0100%比较图像文件指定区域的平均颜色与目标颜色是否在容差范围内
*
* @param imageFile 待分析的图像文件
* @param targetColor 目标颜色
* @param tolerancePercent 容差百分比0100表示允许的最大色差百分比
* @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);
}
/**
* 使用图像处理高斯平滑 + 泡沫过滤和百分比容差0100%
* 分析图像文件指定区域的平均颜色是否与目标颜色匹配
*
* @param imageFile 待分析的图像文件
* @param targetColor 目标颜色
* @param tolerancePercent 容差百分比0100表示允许的最大色差百分比
* @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 差分阈值百分比0100灰度差分阈值 = 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 容差百分比0100
* @return double 绝对距离阈值
* @throws IllegalArgumentException 如果百分比不在 0100 范围内
*/
private double convertTolerance(double percent) {
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("tolerancePercent 范围应在 0–100 之间");
}
return MAX_DISTANCE * (percent / 100.0);
}
}
Loading…
Cancel
Save