pHash 图片去重
用感知哈希(pHash/dHash/aHash)判断两张图片是否相似,用于视频关键帧去重。
#type / concept
#status / growing
#tech / ai
#media / video
[!info] related notes
pHash 图片去重
一句话定义
感知哈希(Perceptual Hash)把图片缩放、灰度化后生成一个短指纹,两张图片的指纹越接近,画面就越相似。用于快速判断视频中相邻帧是否重复。
为什么需要图片去重
视频关键帧提取 中,无论用固定间隔抽帧还是 FFmpeg 场景检测,都可能抽出大量视觉上几乎相同的帧:
- 手部轻微移动,背景不变
- 镜头轻微晃动
- 字幕跳动但画面主体相同
- 快速转场中连续几帧变化都很大但语义上是同一镜头
去重的目标:从候选帧中去掉看起来太像的,只保留画面变化明显的关键帧。
核心机制
感知哈希的三种变体
| 算法 | 原理 | 速度 | 精度 |
|---|---|---|---|
| aHash(平均哈希) | 缩放后计算像素平均值,大于平均为 1,否则为 0 | 最快 | 较低 |
| dHash(差分哈希) | 缩放后比较相邻像素的差值,差值大于 0 为 1 | 快 | 中等 |
| pHash(感知哈希) | 缩放后做 DCT 变换,取低频部分生成哈希 | 较慢 | 最高 |
推荐用 dHash:速度和精度平衡最好,适合视频帧去重场景。
算法流程(以 dHash 为例)
原图
↓
缩放到 9×8 像素
↓
转灰度
↓
逐行比较相邻像素:左 > 右 则为 1,否则为 0
↓
得到 64 位哈希值
相似度判断:汉明距离
两张图的哈希做异或(XOR),统计不同位的数量:
hash_a = 10110100 01001011 ...
hash_b = 10110100 01001111 ...
XOR = 00000000 00000110 ...
汉明距离 = 2
距离越小越相似:
| 汉明距离 | 含义 |
|---|---|
| 0 | 完全相同 |
| 1~5 | 非常相似 |
| 6~10 | 有些差异 |
| 11+ | 明显不同 |
在视频关键帧去重中,通常用 距离 > 8 作为”画面变化明显”的阈值。
Python 实现
使用 imagehash 库
from PIL import Image
import imagehash
def compute_dhash(image_path):
img = Image.open(image_path)
return imagehash.dhash(img)
def are_similar(hash1, hash2, threshold=8):
distance = hash1 - hash2 # 汉明距离
return distance < threshold
关键帧去重流程
import imagehash
from PIL import Image
def deduplicate_frames(frame_paths, threshold=8, max_keyframes=8):
keyframes = []
last_hash = None
for path in frame_paths:
current_hash = imagehash.dhash(Image.open(path))
if last_hash is None:
keyframes.append(path)
last_hash = current_hash
continue
distance = current_hash - last_hash
if distance > threshold:
keyframes.append(path)
last_hash = current_hash
if len(keyframes) >= max_keyframes:
break
return keyframes
不用第三方库的极简实现
from PIL import Image
def dhash(image_path, hash_size=8):
img = Image.open(image_path).convert("L").resize(
(hash_size + 1, hash_size), Image.LANCZOS
)
pixels = list(img.getdata())
bits = []
for row in range(hash_size):
for col in range(hash_size):
idx = row * (hash_size + 1) + col
bits.append(pixels[idx] > pixels[idx + 1])
return int("".join("1" if b else "0" for b in bits), 2)
def hamming_distance(hash1, hash2):
return bin(hash1 ^ hash2).count("1")
阈值选择
| 阈值 | 效果 |
|---|---|
| 5 | 更敏感,保留更多帧,适合内容变化缓慢的视频 |
| 8 | 平衡值,适合大多数短视频 |
| 10 | 更严格,只保留变化较大的帧 |
| 15 | 只保留明显的镜头切换 |
边界与易混淆点
- pHash 不是 AI 语义理解:它只判断像素级相似度,不知道画面内容是什么
- pHash 不能替代视觉模型:去重后仍需 AI 分析每张关键帧的内容
- pHash 对旋转和裁剪敏感:同一画面旋转 90 度后哈希完全不同
- 汉明距离不是百分比:64 位哈希中距离 8 表示 12.5% 的位不同
适用场景
- 视频关键帧去重(本文主要场景)
- 相册去重
- 图片检索(以图搜图)
- 版权检测(初筛)