简介:面向短视频平台内容审核、版权保护与个性化推荐等场景,本项目提出并实现了一套结合多哈希算法与孪生神经网络的相似性检测完整方案。方案将视频关键帧特征转换为哈希码,再通过共享权重的双神经子网络学习度量,兼顾检索速度与精度,能有效应对大规模视频库的相似内容匹配问题。压缩包共277个文件,以py源代码、PDF设计文档、Vue前端页面和HTML交互模板为核心,辅以jpg/png算法流程图、js脚本、XML配置、mp4效果演示及SQLite数据库,整体约77.09MB。内容覆盖从特征提取、哈希编码到相似度度量的完整代码实现,并配有登录注册、视频上传等可运行界面与部署说明,便于读者理解原理并直接复现或扩展。已有64人学习浏览,适合具备Python基础并从事计算机视觉、多媒体检索方向研究的学生、算法工程师及课题开发人员参考。
1. 视频相似度检测不是“相似图片”的简单复用
短视频服务平台每天涌入大量搬运、混剪和二次创作内容,单靠人工审核或依赖视频文件名、MD5 去重早已失效。真正的相似性检测要解决的是“同一个视频被裁剪、加滤镜、调整播放速度、翻转甚至重新配音后,系统仍能判断它和原始视频相关”的问题。常见做法是先用哈希算法把视频降维成指纹,快速排除绝大多数不相关内容,再用神经网络对候选集合做精细比对。这套“多哈希召回 + 双神经网络精排”的架构,既能控制计算成本,又能拿到比单一方法高得多的准确率。本文面向需要处理视频去重、版权识别或推荐场景去重的工程师,重点讲清多哈希如何设计、双神经网络各自承担什么任务,以及从抽帧到输出相似度分数的完整落地路径。
2. 多哈希:为什么一套指纹不够,以及指纹怎么设计
2.1 感知哈希三件套:aHash / pHash / dHash 各自捕捉了什么
视频相似性检测的第一层是“粗筛”,目标不是把相似视频全找出来,而是把明显不相关的视频快速扔掉。粗筛最常用的工具是感知哈希(Perceptual Hash)。与 MD5 或 SHA-1 这类加密哈希不同,感知哈希把视觉内容的“结构特征”压缩成一组比特串,两张图片只要人眼看相似,它们的感知哈希就会接近(汉明距离小)。
业内常见的感知哈希算法有三个:
- aHash(Average Hash):将图像缩放到固定尺寸(通常 8x8 或 16x16),转成灰度图后计算所有像素的均值,每个像素与均值比较,大于记为 1,小于记为 0,组成哈希值。它的计算量最小,速度极快,但对亮度变化、加字幕框这类改动较敏感。
- pHash(Perceptual Hash,DCT 变体):缩放后先做离散余弦变换(DCT),保留低频分量,再对低频部分取均值生成哈希。它对亮度和轻微几何形变的鲁棒性比 aHash 强,是短视频封面等价检测中最常用的方案。
- dHash(Difference Hash):比较相邻像素的灰度差值方向,记录差值的符号。它对亮度变化和轻微的缩放最不敏感,擅长捕捉“构图轮廓”相似的内容。
在短视频场景里,我不推荐只使用其中任何一个。原因是短视频的变换太复杂:竖屏转横屏会裁边,加滤镜会改变色彩分布,加字幕会破坏局部 DCT 系数。单一哈希很容易被某种变换击中,导致同一个视频的指纹跑偏。
2.2 多哈希投票:用“多个弱信号”堆出稳定召回
多哈希的核心设计理念是:每个哈希算法单独看都是弱分类器,但把它们组合起来做投票,能显著提升稳定性。常见的做法是“抽帧后逐帧计算多哈希,分桶索引,再按桶内命中次数投票”。
一个标准的最小实现大致是这样的:
import cv2 import numpy as np from PIL import Image def resize_and_gray(frame, size=(16, 16)): # 统一缩放到 16x16,转灰度,减少后续计算压力 img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) return cv2.resize(img, size, interpolation=cv2.INTER_AREA) def a_hash(frame): img = resize_and_gray(frame, (16, 16)) avg = np.mean(img) # 与均值比较生成 256 位哈希 return (img > avg).flatten().astype(np.uint8) def d_hash(frame): img = resize_and_gray(frame, (17, 16)) diff = img[:, 1:] > img[:, :-1] return diff.flatten().astype(np.uint8) def phash(frame, hash_size=16): # 用 DCT 低频分量生成感知哈希 img = resize_and_gray(frame, (32, 32)) dct = cv2.dct(np.float32(img)) low_freq = dct[:hash_size, :hash_size] avg = np.mean(low_freq) return (low_freq > avg).flatten().astype(np.uint8)代码说明:a_hash把 16x16 灰度图与均值比较生成 256 位哈希;d_hash通过相邻列像素差的方向生成 16x16=256 位;pHash先缩放至 32x32 做 DCT,再取出 16x16 低频块与均值比较。三个哈希各捕捉不同维度的视觉差异,组合后单独一个算法被干扰时,另外两个还能兜住。
实际工程中,每 0.5 秒抽一帧,每帧计算三种哈希后拼成一个 768 位的“指纹串”,再按哈希类型分别写入不同的索引。查询时把待检测视频的帧同样计算三种哈希,分桶取出候选哈希,计算汉明距离小于阈值的帧,然后按视频 ID 聚合投票。只有当多帧、多哈希同时命中时,才判定属于候选集。
2.3 从指纹到索引:汉明距离的边界与近似检索
多哈希方案解决了“怎么算指纹”的问题,但视频库数量上来后,线性比对汉明距离是不可行的。常见的做法是用**局部敏感哈希(LSH)**对每帧的指纹分桶,或直接用支持汉明距离的近似近邻库(如 Faiss 的 IndexBinaryFlat / IndexBinaryIVF)。
IndexBinaryFlat 适合数据量在千万级以内的场景,查询时直接暴力计算汉明距离,准确率 100%,但内存占用较高。IndexBinaryIVF 适合数据量更大或要求低延迟的场景,通过先聚类再查询的方式把召回范围缩小到少数几个桶,速度提升明显,代价是召回率略微下降。
需要注意一个边界问题:感知哈希对“较大幅度的改动”容忍度有限。比如同一个视频被插入了 10 秒无关片段后重新发布,逐帧对比时中间那一整段都会是低匹配结果。此时依赖“整段视频指纹一致”的思路会直接漏检,需要通过“滑动窗口 + 帧级投票”把匹配帧数量抬高后再做聚合判断。哈希层只负责召回候选,不负责真正判定相似——判定交给后面的神经网络。
3. 双神经网络:哈希筛完后的第二道精密筛选
3.1 第一路网络:从关键帧中学习视觉内容表示
多哈希召回的候选集里,可能混着一些“哈希上接近但内容并不相同”的视频,例如同一部电影的两个不同片段,或同一帧画面的不同字幕版本。这一步需要神经网络来做精细化判断,输出一个可比较的内容表示向量。
第一路网络处理的对象是单帧图像。它通常会用一个在图像分类或图像检索任务上预训练的卷积网络(如 ResNet50、EfficientNet)作为骨干,去掉最后的全连接分类层,把倒数第二层的特征向量作为该帧的 embedding。但只做单帧特征提取还不够,因为短视频的“同一帧”往往被施加了缩放、裁剪、加速等变换,直接比对单帧特征会产生大量误判。
工程上更有效的做法是关键帧聚合:先从视频中按均匀间隔抽取关键帧,逐帧提取 embedding,再对这些 embedding 做平均池化或最大池化,得到一个代表整个视频的全局内容表示。这里有一个经验值,均匀抽 8~16 帧通常就够了,帧数再多并不会显著提升准确率,反而增加计算耗时。池化方式的选择很关键:平均池化对“整条视频风格一致”的情况更稳健,最大池化对“只希望抓到几个核心镜头”更敏感。
两段视频的全局内容向量之间的相似度,常用余弦相似度或内积来衡量。实际测试中,阈值设在 0.82~0.9 之间时,能过滤掉大多数不相关视频。但仅靠这一个向量,仍然区分不了“结构不同但画面相似”的情况。
3.2 第二路网络:捕获时序结构与动作节奏
很多被搬运、翻拍或混剪的短视频,在视觉内容上有一定变化,但时序结构高度相似,例如:同一套运镜顺序、同一段音乐卡点节奏、同一组动作编排顺序。第一路网络只做帧级内容表示,它不会关注帧之间的先后关系和整体节奏。因此,需要一个专门的时序网络来处理“顺序”和“动作模式”。
第二路网络可以选用以下几类结构:
- 时间序列模型:将第一路网络提取每一帧的特征按时序组成特征序列,送入 GRU / LSTM 或 Transformer 编码器,输出一个视频级别的时序表示。
- 视频动作识别网络:直接使用 3D-CNN(如 C3D、I3D、SlowFast)从连续帧序列中同时提取空间和时序特征,输出维度稍高的动作表示。
- 光流差分网络:对相邻帧计算光流,训练一个分类/匹配网络,让模型学习“动作模式”而非“画面内容”。
实际落地上,我建议把第一路网络当作“提取器”,第二路网络当作“编码器”。具体流程是:多哈希召回候选视频后,先用第一路网络过滤内容不相关的项,节省第二路网络的计算量,再对候选做时序特征提取与比对。
这里有一个隐藏的关键点。如果两段视频内容完全一样但时间方向被打乱,例如原视频被从头到尾倒放,第一路网络的帧级表示不会察觉问题,但第二路网络必须能识别出这种倒序特征。训练时可以加入“时序顺序是否一致”的二分类损失,也可以使用时序对比学习(例如把帧序列打乱作为负样本),让网络学到真正的时序语义,而不只是画面的静态相似性。
3.3 双网络如何配合,以及相似度得分怎样合成
单用第一路网络可能出现画面相似但内容完全不相关的情况,例如大量美食教程开头都有一幕“食材摆盘”镜头、大量游戏短视频都截取同一个“胜利结算”画面。单用第二路网络则可能把节奏一致但内容不同的视频误判为相似。两个网络配合,就是要同时满足“画面相似”和“结构相似”。
典型的配合方式是串行级联:
- 第一路网络先计算候选视频与查询视频的全局内容余弦相似度,低于阈值 T1 的直接剔除;
- 通过第一路阈值过滤后的候选集,进入第二路网络提取时序 embedding,计算时序相似度 S2;
- 最终相似度得分由两个得分加权合成:
S = 0.65 * S1 + 0.35 * S2或S = S1 * S2(乘法形式惩罚任何一个维度相似度低的情况)。
需要注意的是,加权系数不要拍脑袋定死。应根据业务场景来定:如果目标是打击“直接搬运”,S1 权重要大;如果目标是抓“翻拍抄袭”,S2 权重应该提高。实际操作中,先随机抽取 300~500 个标记好的相似/不相似视频对,在验证集上穷举几组权重组合,选出 F1 最高的那组作为默认值。
训练层面,双神经网络的常见策略是使用对比学习(Contrastive Learning):构造正样本对(同一个视频经过视觉变换后的两个版本)和负样本对(不同视频),分别经过两个网络分支,使用 Triplet Loss 或 InfoNCE Loss 来拉近正样本对距离、推远负样本对距离。训练时要注意 Batch Size 不要太小,经验上 Batch Size 至少 128,否则对比学习的收敛速度和稳定性都比较差。
4. 搭建一个可复现的短视频相似性检测系统
4.1 最小系统架构与运行链路
多哈希和双神经网络的完整系统,本质上是一条“三段式”流水线。第一段负责把视频转成指纹库;第二段负责把入库视频的帧 embedding 和时序 embedding 提取出来;第三段在查询时对候选集做精排。
完整链路如下:
- 视频输入,统一抽帧策略(采样率、最大帧数、关键帧选择);
- 对每一帧做多哈希,写入二进制指纹索引;
- 对同一段视频的帧跑双神经网络提特征,特征写入向量数据库;
- 查询视频同样经过上述三步,先走哈希索引召回候选集;
- 候选集过双神经网络精排,得到最终相似度得分;
- 命中高相似度的视频进入人工审核或自动下架流程。
4.2 查询端关键代码:从哈希召回过渡到神经网络精排
这里给出查询侧的半伪代码实现,重点是展示“哈希召回什么时机交给神经网络”的衔接逻辑:
import numpy as np from sklearn.preprocessing import normalize def extract_video_frames(video_path, skip_interval_secs=0.5): cap = cv2.VideoCapture(video_path) fps = int(cap.get(cv2.CAP_PROP_FPS)) frames = [] cur_frame = 0 step = int(fps * skip_interval_secs) while True: ret, frame = cap.read() if not ret: break if cur_frame % step == 0: frames.append(frame) cur_frame += 1 cap.release() # 限制最多取 32 帧,防止长视频拖垮后续计算 if len(frames) > 32: indices = np.linspace(0, len(frames)-1, 32, dtype=int) frames = [frames[i] for i in indices] return frames def query_short_video(video_path, hasher_index, nets, top_k=50): frames = extract_video_frames(video_path) hash_bits = [combine_hashes(f) for f in frames] # 用多个哈希桶的命中数决定是否进入下一层 candidate_ids = hasher_index.search(hash_bits, threshold_hamming=10) if len(candidate_ids) < 1: return [] # 第一路网络:帧级内容过滤 frame_embeds = [nets['content_net'].predict(f) for f in frames] video_embed = np.mean(frame_embeds, axis=0) content_scores = faiss_index_content.search(normalize(video_embed)[None, :], top_k) candidates_keep = [c for c, s in zip(content_scores[1][0], content_scores[0][0]) if s >= 0.82] if len(candidates_keep) == 0: return [] # 第二路网络:时序编码精排 seq_embed = nets['temporal_net'].predict(frame_embeds) final_scores = faiss_index_temporal.search(normalize(seq_embed)[None, :], len(candidates_keep)) return final_scores代码说明:这里的sleep_interval_secs=0.5表示每半秒抽一帧;16 或 32 帧是特征聚合时的常用上限,再多的帧会让神经网络的推理时间线性增长但准确率增益有限。hasher_index.search内部对每帧哈希做汉明距离匹配,threshold_hamming=10表示如果两帧指纹超过 10 bit 不同就不算匹配。帧级内容过滤阈值 0.82 来自余弦相似度,实际需要按预训练模型微调。
4.3 内存索引与向量库的参数选择
系统跑起来后,搜索性能瓶颈通常在向量库和哈希索引的底层参数上。以 Faiss 为例:
| 存储对象 | 索引类型 | 关键参数 | 适用规模 | 说明 |
|---|---|---|---|---|
| 二进制哈希 | IndexBinaryIVF | nlist(聚类中心数) | 千万级以上 | nlist 设为 sqrt(数据量) 效果较好 |
| 二进制哈希 | IndexBinaryFlat | 无 | 百万至千万级 | 暴力搜索,准确率最高 |
| 内容 embedding | IndexIVFFlat | nlist、nprobe | 十万至百万级 | nprobe 越大召回越高,延迟越高 |
| 时序 embedding | IndexHNSWFlat | M、efConstruction、efSearch | 十万级以内 | M=32 时检索速度与准确率平衡较好 |
这里有一个容易被忽略的细节:IndexIVFFlat 需要把训练集聚类后生成量化器。训练数据必须来自真实业务视频的 embedding 分布,不能拿随机图片的 embedding 来训练,否则聚类中心严重偏移,查询时会大面积漏召回。实践中,训练集至少采样不少于 10 万个视频的 embedding 做聚类,聚类后检查每一类的样本数,如果某些类样本过少,要增大 nlist 或重新采样。
5. 参数调节与排错:影响召回率和误检率的几个关键旋钮
5.1 关键参数的推荐初始值
整个系统的精确度并不只由神经网络决定,抽帧策略和哈希阈值对最终效果的影响同样不可忽视。以短视频场景的常见配置作为参考:
- 抽帧间隔:0.5 秒一帧适合大多数情况。如果视频时长很短(10 秒以内),应改为固定抽取 8 帧,避免长视频和短视频之间因为帧数差异造成匹配率偏低。
- 哈希位长:256 位(16x16)是常用折中。位长太短(如 64 位)会让不好区分的视频落入同一个桶,误召回率飙升;太长(如 1024 位)则对扰动过于敏感,稍加滤镜就匹配不上。
- 汉明距离阈值:256 位哈希下,阈值设在 8~12 位比较合理。经验上有两种情况:纯搬运无改动时,汉明距离通常小于 4;加了水印和字幕但主体未动,距离通常在 5~10;改动较大时距离超过 15。
- 内容网络相似度阈值:余弦 0.82~0.88 之间先跑一个月,记录线上命中视频的人工审核通过率,再反向调整。
- 时序网络相似度阈值:建议初始设为 0.75。因为时序特征的分布比内容特征更“挤”,阈值稍微降低一点,召回率的提升会很显著,误检率则需要配合人工抽检来控制。
5.2 误检与漏检的排查路径
误检和漏检是这个系统上线后最常被投诉的问题,排查方向有固定的优先级顺序。
误检高发时,先看哈希层是不是太“宽”。把某一条误检记录对应的两段视频抽帧后,分别打印每个哈希算法的汉明距离。如果距离在 10 以上仍然被召回,说明阈值太宽,应该提高。如果两个算法各自由一张恰好相似的帧造成投票命中,则需要把投票规则从“任一帧命中即算”改成“连续 3 帧以上命中才进入候选”。
漏检高发时,优先排查抽帧密度。短视频常见的变体是原视频被压缩到很短的时长,按 0.5 秒间隔抽帧后可能只抽出寥寥几帧,加上哈希和神经网络都需要多帧信息,漏检就发生了。另一种常见原因是两个视频分辨率差异巨大,例如一个 1080p 一个 240p。此时在哈希层极易错过,因为高分辨率视频经过缩放后丢失了部分低频结构。应对办法是在提取哈希之前,先把两段视频的控制帧统一缩放到 64x64 的同一尺寸再计算。
关于神经网络的干扰,还有一种情况要特别说明:双神经网络在推理时不接受任意尺寸输入,训练时通常把输入帧统一 resize 到 224x224。如果线上代码把帧直接送入网络而忘了 resize,模型会拿到不符合预期尺寸的数据,多个版本的推理结果会出现抖动。这类问题不会直接报错,但会让同一视频二次查询时输出完全不同的相似度分数。建议在特征提取处加一个尺寸断言,确保每个 batch 的输入都是相同尺寸。
另外,生产环境的推理最好把图像归一化参数固定下来。帧被 resize 后还要执行/ 255.0,再用 ImageNet 预训练模型的均值和标准差对通道做标准化。漏写这一步骤会导致特征分布偏移,使相似度整体偏高或偏低。
6. 用冷启动验证替代盲目调参,让阈值自己找到位置
系统搭建完成后,不要急着用网上的 demo 视频测试,也不要只依赖一两个人工标注样本调阈值。正确起始做法是构建一个“冷启动验证集”,规模不必太大,但结构要能代表真实的线上分布。
验证集应该包含以下三类样本:
- 正样本对:同一个源视频经过裁剪、加滤镜、调速、加字幕、翻转等单一或组合变换后的成对视频,至少 200 对;
- 难负样本对:内容来自同一部电影、同一档直播、同一主题但不同片段的两段视频,至少 200 对;
- 随机负样本对:完全无关的视频对 500 对。
在这套验证集上计算不同阈值组合下的召回率、准确率和 F1。记录一组数据:哈希阈值=10, 内容阈值=0.82, 时序阈值=0.75时的精确率和召回率,然后每次只调整其中一个阈值,观察曲线。最终选择的阈值组合应该是在保持 95% 以上召回率的前提下,取精确率最高的那组。
验证通过后,把同一套验证集固化为回归测试集,每次改动抽帧策略、哈希位数、网络结构参数后重新跑一遍,确保改动没有破坏原有精度。冷启动完成后,再逐步用线上真实数据扩充难负样本集,让系统在持续迭代中更贴近业务分布。
值得最后强调的是,相似性检测系统本质上是一个需要持续维护的系统,哈希算法和神经网络版本都会随业务变化而演进,但验证集的标尺作用不会变,回归测试反而是保证系统长期可靠最划得来的投资。
本文还有配套的精品资源,点击获取