说明
由于实际系统中,UI自动化通过断言元素是否存在不能够满足具体验证要求,需要通过对比预期图片与实际自动化截取图片进行对比,举个栗子以下是我需要通过图片对比进行断言的场景:在地图中显示多个不同的元素,且位置是固定的,如果元素过多每一个进行断言比较麻烦,所以采用这种方式。
工具选型
Java+playwright+(pyhton+OpenCV)
Python+Opencv进行识别相似图片
基本算法概念(便于自己理解后面代码实现)
从机器的角度来说先识别图像的特征,然后再相比。
在图像识别中,颜色特征是最为常用的。(其余常用的特征还有纹理特征、形状特征和空间关系特征等)
其中又分为:
直方图
颜色集
颜色矩
聚合向量
相关图
直方图计算法:
在python中利用opencv中的calcHist()方法获取其直方图数据,返回的结果是一个列表,使用matplotlib,画出了这两张图的直方图数据图
如下:
两张图片的直方图还是比较重合的。所以利用直方图判断两张图片的是否相似的方法就是,计算其直方图的重合程度即可。
不过,这种方法有一个明显的弱点,就是他是按照颜色的全局分布来看的,无法描述颜色的局部分布和色彩所处的位置。
也就是假如一张图片以蓝色为主,内容是一片蓝天,而另外一张图片也是蓝色为主,但是内容却是妹子穿了蓝色裙子,那么这个算法也很可能认为这两张图片的相似的。
缓解这个弱点有一个方法就是利用Image的crop方法把图片等分,然后再分别计算其相似度,最后综合考虑。
图像指纹与汉明距离
在介绍下面其他判别相似度的方法前,先补充一些概念。第一个就是图像指纹
图像指纹和人的指纹一样,是身份的象征,而图像指纹简单点来讲,就是将图像按照一定的哈希算法,经过运算后得出的一组二进制数字。
说到这里,就可以顺带引出汉明距离的概念了。
假如一组二进制数据为101,另外一组为111,那么显然把第一组的第二位数据0改成1就可以变成第二组数据111,所以两组数据的汉明距离就为1
简单点说,汉明距离就是一组二进制数据变成另一组数据所需的步骤数,显然,这个数值可以衡量两张图片的差异,汉明距离越小,则代表相似度越高。汉明距离为0,即代表两张图片完全一样。
如何计算得到汉明距离,请看下面三种哈希算法
平均哈希法(aHash)
此算法是基于比较灰度图每个像素与平均值来实现的
一般步骤:
1.缩放图片,一般大小为8*8,64个像素值。
2.转化为灰度图
3.计算平均值:计算进行灰度处理后图片的所有像素点的平均值,直接用numpy中的mean()计算即可。
4.比较像素灰度值:遍历灰度图片每一个像素,如果大于平均值记录为1,否则为0.
5.得到信息指纹:组合64个bit位,顺序随意保持一致性。
最后比对两张图片的指纹,获得汉明距离即可。
感知哈希算法(pHash)
平均哈希算法过于严格,不够精确,更适合搜索缩略图,为了获得更精确的结果可以选择感知哈希算法,它采用的是DCT(离散余弦变换)来降低频率的方法
一般步骤:
- 缩小图片:32 * 32是一个较好的大小,这样方便DCT计算
- 转化为灰度图
- 计算DCT:利用Opencv中提供的dct()方法,注意输入的图像必须是32位浮点型,所以先利用numpy中的float32进行转换
- 缩小DCT:DCT计算后的矩阵是32 * 32,保留左上角的8 * 8,这些代表的图片的最低频率
- 计算平均值:计算缩小DCT后的所有像素点的平均值。
- 进一步减小DCT:大于平均值记录为1,反之记录为0.
- 得到信息指纹:组合64个信息位,顺序随意保持一致性。
- 最后比对两张图片的指纹,获得汉明距离即可。
差值哈希算法(dHash)
相比pHash,dHash的速度要快的多,相比aHash,dHash在效率几乎相同的情况下的效果要更好,它是基于渐变实现的。
步骤:
- 缩小图片:收缩到9*8的大小,以便它有72的像素点
- 转化为灰度图
- 计算差异值:dHash算法工作在相邻像素之间,这样每行9个像素之间产生了8个不同的差异,一共8行,则产生了64个差异值
- 获得指纹:如果左边的像素比右边的更亮,则记录为1,否则为0. 最后比对两张图片的指纹,获得汉明距离即可。
灰度直方图算法
灰度直方图是描述图像中像素灰度值分布的统计工具,横轴是灰度值(0~255),纵轴是对应灰度值的像素数量。通过对比两张图片的直方图分布,可以判断它们的亮度、对比度等统计特征是否相似。直方图对比的核心是计算两个直方图的相似性,常用的方法有:
相关性(Correlation): 值越接近 1 表示越相似。
卡方检验(Chi-Square): 值越小表示越相似。
巴氏距离(Bhattacharyya): 值越接近 0 表示越相似。
直方图相交(Intersection): 值越大表示越相似。
一般步骤:
- 读取两张图片,转换为灰度图。
- 计算灰度直方图:统计每个灰度级的像素数量。
- 归一化直方图:将直方图转换为概率分布(总和为1)。
- 比较直方图:使用上述方法计算相似性得分。
- 输出相似性结果:根据得分判断图片的相似程度。
三直方图算法(RGB)
三直方图对比是彩色图像分析中的常用方法,通过分别比较图像的红(R)、绿(G)、蓝(B)三个通道的直方图分布,综合判断两幅图像在颜色特征上的相似性。
其核心原理如下:
1、通道独立性
- 彩色图像由RGB三个通道组成,每个通道的直方图独立反映该颜色分量的像素分布。
- 通过对比每个通道的直方图相似性,可以捕捉颜色偏差(如偏红、偏蓝)或色彩分布差异。
2、多维特征联合分析
- 三直方图对比本质上是多维特征对比,需综合三个通道的对比结果,而非简单的单维度统计。
- 例如:一张偏红的图像在红色通道的直方图会显著不同于正常图像,而绿色和蓝色通道可能差异较小。
3、相似性度量方法
- 对每个通道单独使用直方图相似性算法(如相关性、巴氏距离、卡方检验等),最后合并结果。
- 合并方式通常为 加权平均 或 最小值/最大值筛选,具体取决于业务需求。
一般步骤:
1、图像预处理
- 将两幅图像统一为相同的颜色空间(如RGB或BGR,需注意OpenCV默认读取为BGR格式)。
- 调整图像尺寸(可选,若图像分辨率差异较大,可能影响直方图统计)。
2、分离通道并计算直方图
- 分别提取R、G、B三个通道的像素数据。
- 为每个通道计算一维直方图(256 bins,范围0~255)。
3、直方图归一化
对每个通道的直方图进行归一化(通常使用L1归一化),消除图像尺寸差异的影响。
公式:
Normalized Hist [ i ] = Hist [ i ] ∑ j = 0 255 Hist [ j ] \text{Normalized Hist}[i] = \frac{\text{Hist}[i]}{\sum_{j=0}^{255} \text{Hist}[j]} Normalized Hist[i]=∑j=0255Hist[j]Hist[i]
4、单通道相似性计算
- 对每个通道的直方图使用相似性度量算法,得到三个独立得分(R得分、G得分、B得分)。
- 常用算法:
1.相关性(Correlation):值越接近1越相似。
2.巴氏距离(Bhattacharyya):值越接近0越相似。
3.卡方检验(Chi-Square):值越小越相似。
4.直方图相交(Intersection):值越大越相似。
5、结果合并
平均法:直接取三个通道得分的平均值。
F i n a l S c o r e = ScoreR+Score G+Score B 3 \text Final Score= \frac{\text{ScoreR+Score G+Score B}} { \text{3}} FinalScore=3ScoreR+Score G+Score B
加权法:根据业务需求为不同通道分配权重(如人眼对绿色更敏感,可赋予更高权重)。
最小值法:取三个得分中的最小值,确保所有通道均满足相似性要求(适用于严格场景)。
单通道直方图
单通道直方图是统计图像中某一颜色通道(如红、绿、蓝)的像素强度分布的直方图。它反映了该通道内像素值的频率分布,用于分析特定颜色分量的亮度或颜色强度特征。例如:
- 红色通道直方图:显示图像中红色分量的分布。
- 绿色通道直方图:显示绿色分量的分布。
- 蓝色通道直方图:显示蓝色分量的分布。
其核心原理是:
对指定通道的每个像素值(0~255)进行计数,形成该通道的统计直方图。
一般步骤:
以下是计算单通道直方图的通用流程:
1、读取图像并分离通道
读取彩色图像(如BGR格式)。
分离出目标通道(例如红色通道)。
2、计算直方图
统计该通道中每个像素值(0~255)的出现次数。
直方图的横轴为像素值,纵轴为频次。
3、直方图归一化(可选)
将频次转换为概率分布(总和为1),消除图像尺寸差异的影响。
4、可视化或分析
绘制直方图观察分布特征。
与其他图像的同一通道直方图对比,分析相似性或差异。
代码实现
aHash、dHash、pHash算法代码实现
# -*- coding: utf-8 -*-
# 利用python实现多种方法来实现图像识别
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 最简单的以灰度直方图作为相似比较的实现
def classify_gray_hist(image1,image2,size = (256,256)):
# 先计算直方图
# 几个参数必须用方括号括起来
# 这里直接用灰度图计算直方图,所以是使用第一个通道,
# 也可以进行通道分离后,得到多个通道的直方图
# bins 取为16
image1 = cv2.resize(image1,size)
image2 = cv2.resize(image2,size)
hist1 = cv2.calcHist([image1],[0],None,[256],[0.0,255.0])
hist2 = cv2.calcHist([image2],[0],None,[256],[0.0,255.0])
# 可以比较下直方图
plt.plot(range(256),hist1,'r')
plt.plot(range(256),hist2,'b')
plt.show()
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree + (1 - abs(hist1[i]-hist2[i])/max(hist1[i],hist2[i]))
else:
degree = degree + 1
degree = degree/len(hist1)
return degree
# 计算单通道的直方图的相似值
def calculate(image1,image2):
hist1 = cv2.calcHist([image1],[0],None,[256],[0.0,255.0])
hist2 = cv2.calcHist([image2],[0],None,[256],[0.0,255.0])
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree + (1 - abs(hist1[i]-hist2[i])/max(hist1[i],hist2[i]))
else:
degree = degree + 1
degree = degree/len(hist1)
return degree
# 通过得到每个通道的直方图来计算相似度
def classify_hist_with_split(image1,image2,size = (256,256)):
# 将图像resize后,分离为三个通道,再计算每个通道的相似值
image1 = cv2.resize(image1,size)
image2 = cv2.resize(image2,size)
sub_image1 = cv2.split(image1)
sub_image2 = cv2.split(image2)
sub_data = 0
for im1,im2 in zip(sub_image1,sub_image2):
sub_data += calculate(im1,im2)
sub_data = sub_data/3
return sub_data
# 平均哈希算法计算
def classify_aHash(image1,image2):
image1 = cv2.resize(image1,(8,8))
image2 = cv2.resize(image2,(8,8))
gray1 = cv2.cvtColor(image1,cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(image2,cv2.COLOR_BGR2GRAY)
hash1 = getHash(gray1)
hash2 = getHash(gray2)
return Hamming_distance(hash1,hash2)
def classify_pHash(image1,image2):
image1 = cv2.resize(image1,(32,32))
image2 = cv2.resize(image2,(32,32))
gray1 = cv2.cvtColor(image1,cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(image2,cv2.COLOR_BGR2GRAY)
# 将灰度图转为浮点型,再进行dct变换
dct1 = cv2.dct(np.float32(gray1))
dct2 = cv2.dct(np.float32(gray2))
# 取左上角的8*8,这些代表图片的最低频率
# 这个操作等价于c++中利用opencv实现的掩码操作
# 在python中进行掩码操作,可以直接这样取出图像矩阵的某一部分
dct1_roi = dct1[0:8,0:8]
dct2_roi = dct2[0:8,0:8]
hash1 = getHash(dct1_roi)
hash2 = getHash(dct2_roi)
return Hamming_distance(hash1,hash2)
# 输入灰度图,返回hash
def getHash(image):
avreage = np.mean(image)
hash = []
for i in range(image.shape[0]):
for j in range(image.shape[1]):
if image[i,j] > avreage:
hash.append(1)
else:
hash.append(0)
return hash
# 计算汉明距离
def Hamming_distance(hash1,hash2):
num = 0
for index in range(len(hash1)):
if hash1[index] != hash2[index]:
num += 1
return num
if __name__ == '__main__':
img1 = cv2.imread('10.jpg')
cv2.imshow('img1',img1)
img2 = cv2.imread('11.jpg')
cv2.imshow('img2',img2)
degree = classify_gray_hist(img1,img2)
#degree = classify_hist_with_split(img1,img2)
#degree = classify_aHash(img1,img2)
#degree = classify_pHash(img1,img2)
print degree
cv2.waitKey(0)
灰度直方图对比代码实现(Python + OpenCV)
import cv2
import numpy as np
import matplotlib.pyplot as plt
def compare_gray_hist(img1_path, img2_path, plot_hist=False):
"""
灰度直方图对比(支持多种相似性度量)
:param img1_path: 图片1路径
:param img2_path: 图片2路径
:param plot_hist: 是否绘制直方图(默认为否)
:return: 各方法的相似性得分(字典形式)
"""
# 1. 读取图片并转为灰度图
img1 = cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE)
# 检查图片是否读取成功
if img1 is None or img2 is None:
raise ValueError("图片路径错误或无法读取!")
# 2. 计算灰度直方图
hist1 = cv2.calcHist([img1], [0], None, [256], [0, 256])
hist2 = cv2.calcHist([img2], [0], None, [256], [0, 256])
# 3. 归一化直方图(概率分布)
cv2.normalize(hist1, hist1, alpha=1, beta=0, norm_type=cv2.NORM_L1)
cv2.normalize(hist2, hist2, alpha=1, beta=0, norm_type=cv2.NORM_L1)
# 4. 计算不同方法的相似性得分
scores = {
"Correlation": cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL),
"Chi-Square": cv2.compareHist(hist1, hist2, cv2.HISTCMP_CHISQR),
"Bhattacharyya": cv2.compareHist(hist1, hist2, cv2.HISTCMP_BHATTACHARYYA),
"Intersection": cv2.compareHist(hist1, hist2, cv2.HISTCMP_INTERSECT)
}
# 5. 可选:绘制直方图
if plot_hist:
plt.figure(figsize=(10, 5))
plt.plot(hist1, color='blue', label='Image 1')
plt.plot(hist2, color='red', label='Image 2', alpha=0.7)
plt.title("Gray Histogram Comparison")
plt.xlabel("Pixel Value")
plt.ylabel("Normalized Frequency")
plt.legend()
plt.show()
return scores
# 使用示例
if __name__ == "__main__":
# 输入两张测试图片路径
img_path1 = "image1.jpg"
img_path2 = "image2.jpg"
# 计算相似性并打印结果
similarity = compare_gray_hist(img_path1, img_path2, plot_hist=True)
print("===== 灰度直方图对比结果 =====")
print(f"相关性(Correlation): {similarity['Correlation']:.4f} (接近1为相似)")
print(f"卡方检验(Chi-Square): {similarity['Chi-Square']:.4f} (接近0为相似)")
print(f"巴氏距离(Bhattacharyya): {similarity['Bhattacharyya']:.4f} (接近0为相似)")
print(f"直方图相交(Intersection): {similarity['Intersection']:.4f} (越大越相似)")
"""
核心步骤
1、图像读取与灰度转换
使用 cv2.imread 的 cv2.IMREAD_GRAYSCALE 模式直接读取灰度图。
检查图片是否成功加载。
2、直方图计算
cv2.calcHist 计算0~255范围的256级灰度直方图。
3、直方图归一化
使用L1归一化(概率分布),确保不同尺寸图片可比。
相似性计算
4、支持四种OpenCV内置方法:
相关性(Correlation):值域[-1, 1],1表示完全匹配。
卡方检验(Chi-Square):值域[0, +∞),0表示完全匹配。
巴氏距离(Bhattacharyya):值域[0, 1],0表示完全匹配。
直方图相交(Intersection):值域[0, 1],1表示完全匹配。
5、直方图可视化(可选)
使用Matplotlib绘制两张图的直方图叠加对比。
"""
不同方法的适用场景
三直方图对比代码实现(Python + OpenCV)
import cv2
import numpy as np
def compare_rgb_hist(img1_path, img2_path, method=cv2.HISTCMP_CORREL):
# 1. 读取图像并分离通道
img1 = cv2.imread(img1_path)
img2 = cv2.imread(img2_path)
channels = cv2.split(img1) # 分离为BGR三个通道
# 2. 计算每个通道的直方图并归一化
scores = []
for i in range(3): # 遍历B(0)、G(1)、R(2)三个通道
hist1 = cv2.calcHist([img1], [i], None, [256], [0, 256])
hist2 = cv2.calcHist([img2], [i], None, [256], [0, 256])
cv2.normalize(hist1, hist1, norm_type=cv2.NORM_L1)
cv2.normalize(hist2, hist2, norm_type=cv2.NORM_L1)
# 3. 计算相似性得分
score = cv2.compareHist(hist1, hist2, method)
scores.append(score)
# 4. 合并得分(取平均值)
final_score = np.mean(scores)
return final_score
# 使用示例
score = compare_rgb_hist("image1.jpg", "image2.jpg", cv2.HISTCMP_CORREL)
print(f"三直方图相似性(相关性): {score:.4f}")
三直方图典型应用场景
1、UI自动化测试
验证界面渲染是否与设计稿颜色一致(如按钮颜色、背景色调)。
2、图像滤镜检测
判断用户上传的图像是否应用了特定滤镜(如老照片滤镜会改变RGB分布)。
3、视频帧相似性分析
检测视频中连续帧是否因编码错误导致颜色异常。
4、色彩校正评估
对比校正前后图像的RGB直方图,验证色彩平衡效果。
单通道直方图代码实现(Python + OpenCV)
import cv2
import matplotlib.pyplot as plt
def single_channel_histogram(image_path, channel_index=2):
"""
计算并绘制指定通道的直方图
:param image_path: 图像路径
:param channel_index: 通道索引(OpenCV默认BGR顺序,0=Blue,1=Green,2=Red)
"""
# 1. 读取图像并分离通道
img = cv2.imread(image_path)
channel = img[:, :, channel_index] # 提取目标通道(例如Red通道)
# 2. 计算直方图
hist = cv2.calcHist([channel], [0], None, [256], [0, 256])
# 3. 归一化(可选)
cv2.normalize(hist, hist, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
# 4. 可视化
plt.figure()
plt.title(f"Channel {channel_index} Histogram")
plt.xlabel("Pixel Value")
plt.ylabel("Frequency")
plt.plot(hist, color='r' if channel_index == 2 else 'b' if channel_index == 0 else 'g')
plt.xlim([0, 256])
plt.show()
# 示例:分析红色通道(索引为2)
single_channel_histogram("image.jpg", channel_index=2)
"""
关键参数说明
channel_index:
OpenCV默认读取的图像为BGR格式,通道索引如下:
0:蓝色通道(Blue)
1:绿色通道(Green)
2:红色通道(Red)
若图像为其他格式(如RGB),需调整索引顺序。
cv2.calcHist参数:
[channel]:输入的单通道图像。
[0]:通道索引(单通道图像固定为0)。
None:不使用掩码。
[256]:直方图的bin数量(0~255)。
[0, 256]:像素值范围。
"""
Java执行python脚本并传递参数,并且有返回值
方法1:使用 Runtime
使用 Runtime.exec()
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ExecutePythonScript {
public static void main(String[] args) {
try {
// 构建命令,例如:python script.py arg1 arg2
String command = "python script.py arg1 arg2";
Process process = Runtime.getRuntime().exec(command);
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待进程结束并获取返回值
int exitVal = process.waitFor();
System.out.println("Exit Value: " + exitVal);
} catch (Exception e) {
e.printStackTrace();
}
}
}
方法2: 使用 ProcessBuilder
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
public class ExecutePythonScript {
public static void main(String[] args) {
try {
// 构建命令和参数列表,例如:python script.py arg1 arg2
ProcessBuilder builder = new ProcessBuilder();
builder.command("python", "script.py", "arg1", "arg2");
Process process = builder.start();
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待进程结束并获取返回值
int exitVal = process.waitFor();
System.out.println("Exit Value: " + exitVal);
} catch (Exception e) {
e.printStackTrace();
}
}
}
方法3:使用外部库(如 Apache Commons Exec)
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version> <!-- 使用最新版本 -->
</dependency>
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import java.io.ByteArrayOutputStream;
public class PythonExecutor {
public static void main(String[] args) {
try {
// 指定Python解释器和脚本路径
String pythonScriptPath = "/path/to/your/script.py";
String[] pythonCommand = {"python", pythonScriptPath, "arg1", "arg2"};
// 创建命令行对象
CommandLine cmdLine = new CommandLine(pythonCommand[0]);
cmdLine.addArguments(pythonCommand, false);
// 创建执行器并设置超时监控(可选)
DefaultExecutor executor = new DefaultExecutor();
executor.setExitValue(1); // 设置非零退出值被认为是错误(可选)
ExecuteWatchdog watchdog = new ExecuteWatchdog(60 * 1000); // 设置超时为60秒
executor.setWatchdog(watchdog);
// 捕获输出和错误流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
// 执行命令并获取返回值
int exitValue = executor.execute(cmdLine);
System.out.println("Exit Value: " + exitValue);
System.out.println("Output: " + outputStream.toString());
System.out.println("Error: " + errorStream.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
解释代码的关键部分:
CommandLine:用于构建要执行的命令行。这里包括了Python解释器路径和你的脚本路径以及任何参数。
DefaultExecutor:用于执行命令行命令。你可以设置超时监控、输出和错误流处理器等。
ExecuteWatchdog:用于设置命令执行的超时时间,防止命令执行时间过长。
PumpStreamHandler:用于捕获命令执行的输出和错误流。
execute() 方法执行命令并返回退出值。输出和错误流可以通过之前设置的ByteArrayOutputStream获取。
*/
实现思路
编写Java断言工具类
通过传递预期图片与实际图片以及评分线,来实现对两个图片的对比断言
原文链接:https://blog.csdn.net/feimengjuan/article/details/51279629