1 changed files with 263 additions and 0 deletions
@ -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); |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue