<think>我们面临的任务是:使用Python对文件夹内的图片进行基于内容的重复检查(识别相似图片)。根据引用内容,我们可以采用图像哈希的方法来实现。图像哈希能够将图片内容转化为一个固定长度的哈希值,相似的图片会有相似的哈希值(汉明距离较小)。
常见的图像哈希算法包括:
1. 平均哈希(Average Hash, aHash)
2. 感知哈希(Perceptual Hash, pHash)
3. 差异哈希(Difference Hash, dHash)
我们可以使用`imagehash`库来计算这些哈希值,并结合`PIL`(或`Pillow`)库来读取图片。
基本步骤:
1. 遍历指定文件夹及其子文件夹,获取所有图片文件路径(支持常见格式如.jpg, .png等)。
2. 读取每张图片,并计算其哈希值(可以选择一种哈希算法,例如平均哈希)。
3. 比较哈希值:如果两张图片的哈希值相同或汉明距离小于某个阈值(例如5),则认为它们是相似的。
4. 标记重复图片:将重复的图片分组,然后可以选择删除或移动到另一个文件夹。
注意:由于图像哈希对图片的轻微变化(如缩放、格式转换、轻微色彩调整)具有一定的鲁棒性,所以这种方法适合检测视觉上相似的图片。
根据引用[2]和引用[5],我们可以使用字典来存储哈希值和图片路径。但是,由于可能存在哈希碰撞(不同的图片有相同的哈希值)以及相似图片的哈希值可能不完全相同(但汉明距离小),我们需要考虑如何存储和比较。
改进方案:
我们可以使用一个字典,键为哈希值(但这里不能直接用哈希值作为唯一键,因为相似图片的哈希值可能不同),所以我们可以通过分组的方式:将汉明距离小于阈值的图片归为同一组。
然而,为了简单起见,我们可以先使用精确匹配(即哈希值完全相等)来检测重复图片。但这样可能会漏掉一些相似图片。因此,更准确的做法是计算每张图片的哈希值,然后两两比较汉明距离,但这样计算量很大(时间复杂度O(n^2))。
引用[5]提到了一种高效的方法:使用多进程加速,并且将图片缩小到很小的尺寸(如32x32)来计算哈希,这样可以提高速度。同时,我们也可以考虑使用更高效的存储结构(如局部敏感哈希)来减少比较次数,但这里我们为了简单,可以先用一个列表存储所有图片的哈希和路径,然后进行两两比较(当图片数量很大时,可能需要优化)。
考虑到效率,我们可以采用以下折中方法:
1. 使用一个字典,键为哈希值(字符串表示),值为一个列表,存储具有相同哈希值的图片路径(这样可以直接找到完全相同的图片)。
2. 对于相似但不完全相同的图片,我们可以将图片的哈希值(例如64位的哈希值)分成4段,每段16位,然后以每一段作为键,将图片分组。这样,相似的图片可能会在同一组内,然后我们只需要在组内比较汉明距离。这样可以减少比较次数。
但为了简化,我们这里先实现一个基本版本:精确匹配(即哈希值完全相等)和阈值匹配(两两比较,但只比较哈希值汉明距离小于等于阈值的图片)。当图片数量很大时,两两比较可能会很慢,所以我们可以设定一个阈值,比如只比较哈希值的前几位相同的那些图片(局部敏感哈希的思想)。不过,引用[5]中使用了缩小图片尺寸和多进程来提高效率。
根据引用[5]的方案,我们可以这样实现:
- 缩小图片尺寸到(32,32)或(64,64),然后计算哈希值(这样计算速度快,并且对缩放等操作鲁棒)。
- 使用多进程并行计算每张图片的哈希值。
由于时间关系,我们先实现一个单进程版本,并允许用户设置汉明距离阈值。
步骤:
1. 遍历文件夹,收集图片路径。
2. 计算每张图片的哈希值(使用平均哈希或感知哈希)。
3. 将图片按照哈希值分组(分组方式:精确分组,即哈希值完全相同的分为一组)。
4. 对于每组,如果组内图片数量大于1,那么这些图片就是重复的(完全相同的图片)。
5. 但是,我们还要考虑相似但不完全相同的图片。所以,我们还需要比较不同组之间的图片(如果组与组之间的哈希值汉明距离小于阈值,则合并这两组)。这一步比较耗时。
为了平衡,我们可以先输出完全相同的图片,然后对于相似图片,我们可以采用以下方法:
- 将哈希值转换为字符串(二进制形式)后,我们可以构建一个倒排索引,比如取哈希值的前n位作为键,这样哈希值前n位相同的图片会被分到同一组,然后在组内两两比较汉明距离(组内图片数量会少很多)。
这里我们采用另一种简单方法:将所有图片的哈希值放入一个列表中,然后对列表排序。排序后,相邻的哈希值可能比较接近(但汉明距离不一定,因为汉明距离不是数值上的接近,而是二进制位的差异)。所以这种方法并不好。
因此,我们采用分组策略:将哈希值分成若干段(比如4段),然后每一段作为一个桶的键,这样每个图片会属于4个桶。然后,在同一个桶内的图片,我们比较它们的汉明距离。这样,相似的图片有很大概率会落在同一个桶中(至少有一个分段相同)。这种方法称为“分桶”或“分段”。
但是,为了简单实现,我们这里先不做相似图片的检测,只做完全相同图片的检测(即哈希值完全相等)。因为根据引用[2]和[3],它们都是通过哈希值完全相等来判断重复的。对于相似图片,我们可以在后续版本中改进。
另外,引用[5]提到可以跨文件夹检测,我们这里只针对一个文件夹。
代码实现:
我们将使用`imagehash`库和`Pillow`库。如果没有安装,需要先安装:
```
pip install Pillow imagehash
```
注意:`imagehash`库需要`scipy`,但最新版本可能不再需要,如果遇到问题可以安装`scipy`。
我们将实现以下功能:
1. 遍历文件夹,获取图片路径。
2. 计算每张图片的平均哈希(aHash)。
3. 使用字典记录哈希值对应的图片列表。
4. 遍历字典,输出重复图片(即哈希值相同的图片)。
对于相似图片(哈希值不同但汉明距离小),我们可以扩展:在同一个字典分组内,我们无法直接得到相似图片,所以我们需要额外的步骤:将每个哈希值与字典中所有其他哈希值比较?这不可行,因为这样又是O(n^2)。因此,我们暂时只处理完全相同的图片。
如果用户需要检测相似图片,我们可以采用以下方法:
- 计算所有图片的哈希值,并存储在一个列表中。
- 对于每个图片,我们将其哈希值与之前已经分组的图片进行比较(使用汉明距离),如果与某个组的代表图片(或该组内任意一张图片)的汉明距离小于阈值,则将其归入该组。但这样仍然需要比较很多次。
引用[5]中提到了一个高效的方法:使用多进程和缩小图片尺寸。同时,他们可能采用了更高级的算法,但我们这里先不展开。
因此,我们先提供检测完全相同图片的代码,然后如果有需要再扩展相似图片检测。
代码示例(基于引用[2]):
注意:我们这里只处理一个文件夹,包括子文件夹。
我们将使用平均哈希(aHash)作为示例。
步骤:
1. 遍历文件夹,获取所有图片。
2. 计算每张图片的哈希值。
3. 使用字典,键为哈希值,值为图片路径列表。
4. 如果某个哈希值对应多个图片,则这些图片是重复的(内容完全相同)。
但是,注意:即使图片内容完全相同,如果格式不同(如一张是jpg,一张是png),但内容相同,计算出的哈希值也会相同。
代码:
```python
import os
from PIL import Image
import imagehash
def find_duplicate_images(folder_path):
# 存储哈希值和对应的图片路径列表
hash_dict = {}
# 支持的图片格式
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']
# 遍历文件夹
for root, dirs, files in os.walk(folder_path):
for file in files:
# 检查文件扩展名
ext = os.path.splitext(file)[1].lower()
if ext in image_extensions:
img_path = os.path.join(root, file)
try:
# 打开图片
with Image.open(img_path) as img:
# 计算平均哈希
hash_val = imagehash.average_hash(img)
# 将哈希对象转换为字符串,以便作为字典的键
hash_str = str(hash_val)
except Exception as e:
print(f"无法处理图片: {img_path}, 错误: {e}")
continue
# 将图片路径添加到对应的哈希值列表中
if hash_str in hash_dict:
hash_dict[hash_str].append(img_path)
else:
hash_dict[hash_str] = [img_path]
# 找出重复的图片:即哈希值对应多个图片路径的
duplicates = {hash_str: paths for hash_str, paths in hash_dict.items() if len(paths) > 1}
return duplicates
if __name__ == "__main__":
folder_path = "your_image_folder_path" # 替换为你的图片文件夹路径
duplicates = find_duplicate_images(folder_path)
if duplicates:
print("发现重复图片:")
for hash_val, paths in duplicates.items():
print(f"哈希值: {hash_val}, 重复图片数量: {len(paths)}")
for path in paths:
print(f" - {path}")
else:
print("未找到重复图片。")
```
这个代码可以找到内容完全相同的图片(即使文件名不同,存储路径不同)。但无法找到相似图片(内容不完全相同,但视觉上相似)。
如果我们需要检测相似图片,我们可以改进:在得到所有图片的哈希值后,我们计算每两个哈希值之间的汉明距离(但避免重复比较)。我们可以使用一个并查集或者连通分量算法,将汉明距离小于阈值的图片归为一组。
改进版本(检测相似图片)的思路:
1. 计算所有图片的哈希值(使用感知哈希pHash,它对轻微变化更鲁棒)。
2. 构建一个图,节点是图片,如果两张图片的哈希值的汉明距离小于等于阈值(例如5),则在这两个节点之间添加一条边。
3. 使用并查集或深度优先搜索找出所有连通分量,每个连通分量就是一组相似图片。
但是,这个操作的时间复杂度是O(n^2),当图片数量很大时,非常慢。
因此,我们可以采用近似方法:将每个哈希值分成多个片段,然后构建倒排索引,每个片段作为一个桶。然后只在同一个桶内的图片之间进行比较。这样,我们只需要比较同一个桶内的图片,减少了比较次数。
由于时间关系,这里不展开。我们提供一个基本思路的代码框架,但要注意,当图片数量很大时,可能仍然很慢。
我们使用感知哈希(pHash)并设置汉明距离阈值为5(可调)来检测相似图片:
步骤:
1. 计算每张图片的感知哈希值(使用`imagehash.phash`)。
2. 将哈希值存储在一个列表中,并记录对应的图片路径。
3. 创建一个并查集数据结构(用于合并相似的图片组)。
4. 遍历所有图片,对于每张图片,再遍历其他图片(为了避免重复,可以从当前图片的下一个开始),计算汉明距离。如果汉明距离<=阈值,则将这两张图片合并到一组(使用并查集)。
5. 最后,输出并查集中的所有分组(每组图片都是相似的)。
但是,这个双重循环在图片数量大时非常慢(比如10000张图片,需要比较约5000万次)。因此,我们考虑优化:使用多进程,或者使用更高效的数据结构(如局部敏感哈希)。
这里,我们为了效率,只实现一个简单版本,并警告用户图片数量多时可能很慢。
代码示例(检测相似图片):
注意:此代码在图片数量多时效率很低,仅适用于小规模图片集。
```python
import os
from PIL import Image
import imagehash
from collections import defaultdict
def hamming_distance(hash1, hash2):
# 计算两个哈希值之间的汉明距离
return hash1 - hash2 # imagehash的哈希对象支持直接相减得到汉明距离
def find_similar_images(folder_path, threshold=5):
# 存储所有图片的哈希值和路径
hash_list = [] # 元素为元组 (hash, path)
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']
# 遍历文件夹,计算哈希
for root, dirs, files in os.walk(folder_path):
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext in image_extensions:
img_path = os.path.join(root, file)
try:
with Image.open(img_path) as img:
# 使用感知哈希
hash_val = imagehash.phash(img)
hash_list.append((hash_val, img_path))
except Exception as e:
print(f"无法处理图片: {img_path}, 错误: {e}")
continue
# 使用并查集来合并相似图片组
# 初始化并查集:每个图片自成一类,用索引表示
parent = list(range(len(hash_list)))
rank = [0] * len(hash_list)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
rx, ry = find(x), find(y)
if rx == ry:
return
if rank[rx] < rank[ry]:
parent[rx] = ry
elif rank[rx] > rank[ry]:
parent[ry] = rx
else:
parent[ry] = rx
rank[rx] += 1
# 双重循环比较哈希值
n = len(hash_list)
for i in range(n):
for j in range(i+1, n):
dist = hamming_distance(hash_list[i][0], hash_list[j][0])
if dist <= threshold:
# 合并i和j
union(i, j)
# 分组
groups = defaultdict(list)
for i in range(n):
root = find(i)
groups[root].append(hash_list[i][1])
# 过滤掉单独图片(没有重复的)
similar_groups = [group for group in groups.values() if len(group) > 1]
return similar_groups
if __name__ == "__main__":
folder_path = "your_image_folder_path"
threshold = 5 # 汉明距离阈值,可调整
similar_groups = find_similar_images(folder_path, threshold)
if similar_groups:
print(f"发现相似图片组(汉明距离<={threshold}):")
for i, group in enumerate(similar_groups, 1):
print(f"组 {i}:")
for path in group:
print(f" - {path}")
else:
print("未找到相似图片。")
```
注意:这个代码在图片数量大时非常慢,不推荐用于大规模图片集。对于大规模图片,建议使用局部敏感哈希(LSH)等近似算法。
综上所述,我们提供了两种方法:
1. 检测完全相同的图片(高效,使用精确哈希匹配)。
2. 检测相似图片(低效,使用两两比较汉明距离,适用于小规模图片集)。
在实际应用中,可以根据需求选择。
另外,引用[5]提到了一种高效的方法,使用多进程和缩小图片尺寸。我们可以尝试使用多进程来加速计算哈希值,但比较部分(双重循环)仍然很慢。因此,对于相似图片检测,我们建议使用分桶策略来减少比较次数。
由于时间限制,这里不再展开分桶策略的实现。用户可以根据需要自行优化。
最后,我们提醒用户,图像哈希方法并不能保证100%准确,可能会有误判(将不相似的图片判为相似)或漏判。在实际使用中,可以调整阈值或尝试不同的哈希算法。
希望这些信息对你有帮助。
相关问题:
1. 如何提高相似图片检测的效率?
2. 图像哈希方法有哪些?它们之间有什么区别?
3. 除了图像哈希,还有哪些方法可以检测相似图片?
4. 如何处理图片数量很大的情况?
5. 如何删除重复图片并保留一份?
</think>
以下是使用 Python 对文件夹内图片进行基于内容的重复检查(识别相似图片)的完整方法,结合图像哈希技术和高效处理流程:
### 核心原理
图像哈希算法将图片内容转换为固定长度的指纹(哈希值),相似图片的哈希值汉明距离小。常用算法:
- **平均哈希 (aHash)**:适合快速检测完全相同的图片
- **感知哈希 (pHash)**:对缩放、格式转换鲁棒,适合相似图片识别
- **差异哈希 (dHash)**:对色彩变化敏感
### 完整解决方案
```python
from PIL import Image
import imagehash
import os
from collections import defaultdict
import numpy as np
def find_duplicate_images(folder_path, hash_size=8, threshold=5, hash_func='phash'):
"""
基于内容识别相似图片
参数:
folder_path: 图片文件夹路径
hash_size: 哈希尺寸(越大越精确, 默认8)
threshold: 汉明距离阈值(0-64, 越小越严格)
hash_func: 哈希算法('ahash','phash','dhash')
"""
# 选择哈希算法
hash_mapper = {
'ahash': imagehash.average_hash,
'phash': imagehash.phash,
'dhash': imagehash.dhash
}
hash_function = hash_mapper.get(hash_func, imagehash.phash)
# 存储哈希值和路径
hash_dict = defaultdict(list)
supported_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff']
# 遍历文件夹
for root, _, files in os.walk(folder_path):
for file in files:
if any(file.lower().endswith(ext) for ext in supported_exts):
img_path = os.path.join(root, file)
try:
with Image.open(img_path) as img:
# 生成哈希值
hash_val = hash_function(img, hash_size)
hash_dict[hash_val].append(img_path)
except Exception as e:
print(f"处理失败: {img_path} - {str(e)}")
# 分组相似图片
groups = []
visited = set()
hashes = list(hash_dict.keys())
for i, hash1 in enumerate(hashes):
if hash1 in visited:
continue
group = []
for path in hash_dict[hash1]:
group.append(path)
visited.add(hash1)
# 查找相似哈希
for j in range(i+1, len(hashes)):
hash2 = hashes[j]
if hash2 in visited:
continue
# 计算汉明距离
hamming_dist = hash1 - hash2
if hamming_dist <= threshold:
for path in hash_dict[hash2]:
group.append(path)
visited.add(hash2)
if len(group) > 1:
groups.append(group)
return groups
if __name__ == "__main__":
# 使用示例
duplicates = find_duplicate_images(
folder_path="/path/to/your/images",
hash_size=8,
threshold=5, # 推荐值: 相同图片=0, 相似图片≤5
hash_func='phash' # 推荐phash
)
# 打印结果
print(f"发现 {len(duplicates)} 组重复图片:")
for i, group in enumerate(duplicates, 1):
print(f"\n组 #{i}:")
for path in group:
print(f" - {path}")
```
### 关键步骤说明
1. **图像预处理**(自动完成)
- 转换为灰度图减少颜色干扰
- 缩小到固定尺寸(`hash_size`控制)
- 计算像素平均值(哈希算法基础)
2. **哈希值计算**(核心步骤)
- `phash`算法流程:
```math
\text{1. 缩小至}8\times8\text{灰度图}
\]
\text{2. 计算DCT变换:}D(i,j)=\sum_{x=0}^{7}\sum_{y=0}^{7} \text{img}(x,y)\cos\left[\frac{\pi}{8}(x+\frac{1}{2})i\right]\cos\left[\frac{\pi}{8}(y+\frac{1}{2})j\right]
\]
\text{3. 取低频分量(左上角}8\times8\text{)}
\]
\text{4. 比较像素与均值:哈希位}= \begin{cases} 1 & \text{像素} \geq \text{均值} \\ 0 & \text{否则} \end{cases}
\]
```
3. **相似度判定**
- 汉明距离公式:$H(h_1,h_2) = \sum_{i=0}^{n} |bit_i(h_1) - bit_i(h_2)|$
- 阈值选择建议:
- 完全相同图片:阈值=0
- 相似图片:阈值≤5(64位哈希)
- 调整灵敏度:增大`threshold`放宽标准
### 优化技巧
1. **性能提升**
```python
# 添加以下优化代码
from multiprocessing import Pool
def process_image(args):
file, root = args
# ...(哈希计算逻辑)
# 多进程处理
with Pool(processes=os.cpu_count()) as pool:
results = pool.map(process_image, [(f, r) for r,_,f in os.walk(folder_path) for f in files])
```
2. **误报处理**
- 增大`hash_size`(16或32)
- 组合多种哈希算法:
```python
ahash = imagehash.average_hash(img)
dhash = imagehash.dhash(img)
combined_hash = str(ahash) + str(dhash) # 组合哈希
```
3. **结果处理**
```python
# 自动删除重复项(保留最早文件)
for group in duplicates:
keep = group[0] # 保留第一个
for path in group[1:]:
os.remove(path)
print(f"已删除: {path}")
```
### 算法对比表
| 哈希类型 | 计算速度 | 旋转鲁棒性 | 色彩鲁棒性 | 缩放鲁棒性 | 适用场景 |
|---------|---------|-----------|-----------|-----------|---------|
| aHash | ⚡️⚡️⚡️⚡️ | ⚪️ | ⚪️ | ⚪️ | 完全重复 |
| pHash | ⚡️⚡️⚡️ | ⚪️ | ⚡️⚡️ | ⚡️⚡️⚡️ | 相似图片 |
| dHash | ⚡️⚡️⚡️⚡️ | ⚪️ | ⚡️ | ⚡️⚡️ | 内容比对 |
> **实践建议**:对10,000+图片集,使用`phash+多进程`组合,处理时间约3-5分钟[^5]
### 常见问题解决
1. **内存不足**:
- 添加图片尺寸限制:`img = img.resize((800,800))`
- 使用生成器替代列表存储路径
2. **漏识别问题**:
```python
# 增加颜色直方图比对
hist1 = img1.histogram()
hist2 = img2.histogram()
similarity = sum((a - b)**2 for a, b in zip(hist1, hist2)) / len(hist1)
```
3. **特殊格式支持**:
```python
# 添加HEIC格式支持
if file.lower().endswith('.heic'):
import pyheif
heif_file = pyheif.read(img_path)
img = Image.frombytes(heif_file.mode, heif_file.size, heif_file.data)
```