参考文献
- 图片相似度计算方法总结 - 知乎 (zhihu.com)
- Python OpenCV 视觉特征提取和匹配 - 知乎 (zhihu.com)
- 图像相似度中的Hash算法 - Yumeka - 博客园 (cnblogs.com)
- 汉明距离及其高效计算方式 (zhihu.com)
开源仓库
- https://github.com/python-pillow/Pillow
- https://github.com/cybereality/Perception
- thorn-oss/perception: Perceptual hashing tools for detecting child sexual abuse material (github.com)
实现案例
比较两张图片的相似度,支持对图片进行分块裁剪之后比较每块的相似度。默认是整张图片比较。
安装依赖
pip install perception=0.6.8
pip install Pillow=9.4.0
代码实现
import itertools
import math
from typing import NamedTuple
import PIL
from PIL import Image
from perception import hashers
class ImageBlock(NamedTuple):
row: int
colum: int
image_block: Image
def __str__(self):
return '(%d, %d)' % (self.row, self.colum)
class ImageBlockDistance(NamedTuple):
left_image_block: ImageBlock
right_image_block: ImageBlock
distance: float
def __str__(self):
return '%s vs %s distance(%f)' % (self.left_image_block, self.right_image_block, self.distance)
class ImageDistance(NamedTuple):
left_image_path: str
right_image_path: str
distances: list
class ImageCompare:
def __init__(self, left_image, right_image, hasher=hashers.PHash(), block=1):
self.left_image = left_image
self.right_image = right_image
self.hasher = hasher
self.block = block
self.__row_columns_size = int(math.sqrt(self.block))
def similarity(self, threshold=0.5, ratio=0.75, enable_detail=False):
"""
获取图片相似度信息
:param threshold: 阈值
:param ratio: 符合阈值的图片块占总图片块的比例
:param enable_detail: 是否记录图片块详情
:return: 相似度信息
"""
image_distance = ImageCompare.compute_distance(self.left_image, self.right_image, self.__row_columns_size,
self.hasher)
threshold_match = 0
details = []
total_distance = 0
for item in image_distance.distances:
total_distance = total_distance + item.distance
if item.distance <= threshold:
threshold_match = threshold_match + 1
if enable_detail:
details.append(str(item))
else:
details = None
match_ratio = threshold_match / len(image_distance.distances)
return {
"match": match_ratio >= ratio,
"match_number": threshold_match,
"match_ratio": match_ratio,
"total_distance": total_distance,
"detail_distance": details
}
def __str__(self):
return "left_image:%s right_image:%s hasher:%s, block:%s row_columns_size:%s" % (
self.left_image, self.right_image, self.hasher.__class__.__name__, self.block, self.__row_columns_size)
@staticmethod
def compute_distance(left_image_path, right_image_path, row_columns_size=1, hasher=hashers.PHash()):
"""
计算图片HammingDistance
:param left_image_path: 图片1路径
:param right_image_path: 图片2路径
:param row_columns_size: 图片行列数
:param hasher: 图片比对算法
:return: 图片距离
"""
left_image_blocks = ImageCompare.image_crop_blocks(Image.open(left_image_path), row_columns_size)
right_blocks = ImageCompare.image_crop_blocks(Image.open(right_image_path), row_columns_size)
distances = []
for item in list(zip(left_image_blocks, right_blocks)):
left_hash_value, right_hash_value = hasher.compute(item[0].image_block), hasher.compute(item[1].image_block)
distance = hasher.compute_distance(left_hash_value, right_hash_value)
distances.append(ImageBlockDistance(left_image_block=item[0], right_image_block=item[1], distance=distance))
return ImageDistance(left_image_path=left_image_path, right_image_path=left_image_path,
distances=distances)
@staticmethod
def image_crop_blocks(image: PIL.Image, n=1):
"""
图片裁剪成块
:param image:
:param n: 行列数(总块数=n^2)
:return: 图片块集合
"""
image_blocks = []
if n == 1:
image_blocks.append(ImageBlock(row=0, colum=0, image_block=image))
else:
width, height = image.size
size = min(width, height)
left, top, right, bottom = (int((width - size) / 2), int((height - size) / 2),
int((width + size) / 2), int((height + size) / 2))
square_image = image.crop((left, top, right, bottom))
min_size = size // n
for i in range(n):
for j in range(n):
left, top, right, bottom = (j * min_size, i * min_size, (j + 1) * min_size, (i + 1) * min_size)
image_block = square_image.crop((left, top, right, bottom))
image_blocks.append(ImageBlock(row=i, colum=j, image_block=image_block))
return image_blocks
测试用例
示例图片:人眼识别其中 device_yes_01.jpg和device_yes_02.jpg相似,device_no_01.jpg和device_no_02.jpg
用例A
测试参数:[block]图片分块为1,表示整体比较; [threshold]阈值:0.45 表示图片相似度距离小于0.45认为相似。
def path(name):
return './assets/' + name
if __name__ == '__main__':
image_name_set = ['device_yes_01.jpg', 'device_yes_02.jpg', 'device_no_01.jpg', 'device_no_02.jpg']
for g in [(path(item[0]), path(item[1])) for item in
list(itertools.combinations_with_replacement(image_name_set, 2))]:
left_image_path, right_image_path = g[0], g[1]
print(left_image_path, " vs ", right_image_path)
x = ImageCompare(left_image_path, right_image_path, block=1)
# print(x)
print(x.similarity(threshold=0.45, enable_detail=False))
print("--------------------------")
输出结果:从结果上分析和人眼识别的结论是一致的。其中:device_yes_01.jpg vs device_yes_02.jpg 的距离0.3437 远小于
device_yes_01.jpg vs device_no_01.jpg 的距离0.53125,这组参数配置能够区分出来图片的相似性。
./assets/device_yes_01.jpg vs ./assets/device_yes_01.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_yes_02.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.34375, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_no_01.jpg
{'match': False, 'match_number': 0, 'match_ratio': 0.0, 'total_distance': 0.53125, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_no_02.jpg
{'match': False, 'match_number': 0, 'match_ratio': 0.0, 'total_distance': 0.53125, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_yes_02.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_no_01.jpg
{'match': False, 'match_number': 0, 'match_ratio': 0.0, 'total_distance': 0.65625, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_no_02.jpg
{'match': False, 'match_number': 0, 'match_ratio': 0.0, 'total_distance': 0.59375, 'detail_distance': None}
--------------------------
./assets/device_no_01.jpg vs ./assets/device_no_01.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_no_01.jpg vs ./assets/device_no_02.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.125, 'detail_distance': None}
--------------------------
./assets/device_no_02.jpg vs ./assets/device_no_02.jpg
{'match': True, 'match_number': 1, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
用例B
测试参数:[block]图片分块为16(4*4); [threshold]阈值:0.45 表示图片相似度距离小于0.45认为相似; [ratio]相似图片块占比:0.75 表示匹配。
def path(name):
return './assets/' + name
if __name__ == '__main__':
image_name_set = ['device_yes_01.jpg', 'device_yes_02.jpg', 'device_no_01.jpg', 'device_no_02.jpg']
for g in [(path(item[0]), path(item[1])) for item in
list(itertools.combinations_with_replacement(image_name_set, 2))]:
left_image_path, right_image_path = g[0], g[1]
print(left_image_path, " vs ", right_image_path)
x = ImageCompare(left_image_path, right_image_path, block=16, )
# print(x)
print(x.similarity(threshold=0.45, ratio=0.75, enable_detail=False))
print("--------------------------")
输出结果:从结果上分析相同图片,或者非常近似的图片结论和人眼判断的一致。另外存在无法区分的图片,比如:device_yes_01.jpg vs device_yes_02.jpg的结果与device_yes_01.jpg vs device_no_01.jpg的结果很接近,而且给出了都不相似的结论。这里主要的干扰因素有:block,threshold, ratio。所以进行图片分块的相似度比较之后的结果二次处理不能如上实现简单的比较来完成。比如:两组图片块的结果分别是0.1512和0.4420,它们都符合阈值设置,但是对整体图片的相似度的影响(权重)明显是不同的。
./assets/device_yes_01.jpg vs ./assets/device_yes_01.jpg
{'match': True, 'match_number': 16, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_yes_02.jpg
{'match': False, 'match_number': 6, 'match_ratio': 0.375, 'total_distance': 7.8125, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_no_01.jpg
{'match': False, 'match_number': 6, 'match_ratio': 0.375, 'total_distance': 7.5625, 'detail_distance': None}
--------------------------
./assets/device_yes_01.jpg vs ./assets/device_no_02.jpg
{'match': False, 'match_number': 4, 'match_ratio': 0.25, 'total_distance': 7.75, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_yes_02.jpg
{'match': True, 'match_number': 16, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_no_01.jpg
{'match': False, 'match_number': 5, 'match_ratio': 0.3125, 'total_distance': 7.90625, 'detail_distance': None}
--------------------------
./assets/device_yes_02.jpg vs ./assets/device_no_02.jpg
{'match': False, 'match_number': 3, 'match_ratio': 0.1875, 'total_distance': 7.96875, 'detail_distance': None}
--------------------------
./assets/device_no_01.jpg vs ./assets/device_no_01.jpg
{'match': True, 'match_number': 16, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
./assets/device_no_01.jpg vs ./assets/device_no_02.jpg
{'match': True, 'match_number': 15, 'match_ratio': 0.9375, 'total_distance': 5.40625, 'detail_distance': None}
--------------------------
./assets/device_no_02.jpg vs ./assets/device_no_02.jpg
{'match': True, 'match_number': 16, 'match_ratio': 1.0, 'total_distance': 0.0, 'detail_distance': None}
--------------------------
建议
通常情况下建议采用用例A的方式进行图片相似性比较,通过大量的图片测试进行阈值的调节来找到合适的设置。图片分块的比较复杂度较高,特别是影响因子比整体图片比较多,而且二次处理上需要根据应用场景来设计。
标签:distance,基于,assets,image,hammingdistance,jpg,pHash,device,match From: https://blog.51cto.com/aiilive/7539855