资讯详情

资讯详情

基于CNN特征提取的本地图片视频重复检测与整理工具设计

1. 从一次硬盘大扫除说起为什么传统哈希方案搞不定重复图片视频前阵子帮朋友整理他那块快满的4T硬盘里面堆了五六年的照片、手机视频、网剧缓存光是图片重名副本和同一段视频的不同压缩版本就占了接近1TB。一开始我想偷懒用常见的感知哈希pHash、aHash、dHash方案跑一遍结果发现这套老办法对一模一样但格式不同的文件还好使一旦遇到同一张图被压缩过、加水印、截过边、调过色以及同一个视频重新压制、缩放、去掉片头片尾这些真实场景误判和漏判的比例高得让人头疼。这就是我决定从头写一个基于CNN特征提取的本地图片视频重复检测与整理工具的直接原因。简单说这个工具的思路是不再拿像素级别的指纹去比对而是让卷积神经网络CNN先看懂图片的内容把每张图压缩成一条高维特征向量再用特征向量之间的距离来判断两张图是不是同一个东西最后把检测出的重复文件自动分组、去重、归档。这篇文章我会完整拆解这个工具的设计思路、模型选型依据、核心代码实现、视频帧采样策略以及在真实数据集上跑的实测结果和一堆踩坑记录。无论你是想处理个人照片库的普通用户还是打算把这套流程集成进自己项目的开发者应该都能从中找到可以直接复用的部分。2. CNN特征提取的核心思路与模型选型2.1 从指纹对比到语义特征对比传统感知哈希的思路是把图片缩小、灰度化、算离散余弦变换DCT再把低频信息压缩成一串64位或256位的二进制串。这个指纹对轻微的全局缩放、格式转换还算鲁棒但它本质上是在描述一张图片的全局统计特征而不是内容结构。举个最典型的例子一张日出照片原图是1600万像素的JPG有一个修复版是720p的PNG还有一个被某聊天软件自动压缩过还加了底部白边的版本。这三个文件的pHash相似度可能只有70%多因为加白边这个操作改变了整张图的像素分布DCT低频系数全变了。但你拿肉眼去看它仨就是同一张图。CNN的做法完全不同。卷积神经网络在ImageNet等大规模数据集上训练后中间层已经学会了检测边缘、纹理、物体部件、物体整体这些层次化的视觉特征。我们把图片输入网络后取最后一个卷积层输出的特征图经过全局池化变成一条向量——这条向量记录的是这张图里有什么结构、什么物体、什么布局而不是这张图的像素长什么样。所以CNN特征天然对缩放、压缩、轻微调色、裁剪边缘、加白边这类操作有很强的鲁棒性。因为这些操作改变了像素值但没改变内容语义。这正是我们做重复检测时真正关心的东西。2.2 主干网络选择ResNet50还是EfficientNet这个工具的核心模块就是特征提取器主干网络的选择直接决定了准确率上限和跑批速度。我实测对比了几种主流模型结论如下模型特征维度单张图片推理耗时(CPU)单张图片推理耗时(GPU)重复检测准确率VGG164096约420ms约20ms中等特征偏底层ResNet502048约180ms约8ms较高EfficientNet-B41792约260ms约12ms最高MobileNetV3-Large1280约90ms约4ms中上我最终选择了ResNet50作为默认配置理由有三条第一ResNet50在torchvision里有现成的预训练权重不需要额外处理开箱即用。第二2048维特征向量在相似度计算和聚类阶段是一个比较舒服的维度既能表达足够丰富的语义信息又不至于像VGG那样4096维导致存储压力和计算开销都偏大。第三ResNet的残差结构在特征提取时比较稳提取出的特征分布相对集中后续做余弦相似度计算时阈值更容易调。EfficientNet的准确率确实更高一些但它在CPU上跑的速度劣势明显。如果你的库里有几十万张图片用EfficientNet跑全量特征提取的时长会让人崩溃。我目前的工具把主干网络做成了可配置项默认ResNet50要求高精度时可以切换EfficientNet-B4跑小规模库。2.3 特征池化与向量归一化的细节很多人用torchvision提取特征时会犯一个错误直接拿model.fc层的输出当特征向量。这个做法的问题在于最后一层全连接层是针对ImageNet的1000类分类任务训练的它的输出分布已经过了一层softmax的挤压区分度反而变差。正确做法是取最后一个卷积阶段的输出做全局平均池化Global Average Pooling。用ResNet50举例输入图片经过conv5_x阶段后输出的是7x7x2048的特征图对空间维度做平均池化后得到2048维向量。这一步在torchvision里可以通过注册forward_hook来实现或者直接把模型改成model.avgpool和model.fc之间的输出。import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image import numpy as np class FeatureExtractor: def __init__(self, model_nameresnet50, devicecuda): self.device torch.device(device if torch.cuda.is_available() else cpu) if model_name resnet50: self.model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) feature_dim 2048 elif model_name efficientnet_b4: self.model models.efficientnet_b4(weightsmodels.EfficientNet_B4_Weights.IMAGENET1K_V1) feature_dim 1792 # 去掉分类层保留到全局池化之后的特征 self.model nn.Sequential(*list(self.model.children())[:-1]) # 对ResNet有效 self.model.to(self.device) self.model.eval() # 预处理与训练时保持一致 self.preprocess transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def extract(self, image_path): img Image.open(image_path).convert(RGB) img_tensor self.preprocess(img).unsqueeze(0).to(self.device) with torch.no_grad(): feature self.model(img_tensor) # 形状: (1, 2048, 1, 1) - 展平成2048维 feature feature.flatten().cpu().numpy() # L2归一化让余弦相似度计算更方便 feature feature / np.linalg.norm(feature) return feature这里有个关键动作是L2归一化。归一化之后两个向量之间的余弦相似度就等于它们的点积可以直接用np.dot(v1, v2)来计算而不用再除以模长。更重要的是归一化让所有特征向量都落在同一个超球面上统一了尺度后续设置相似度阈值时不会因为不同图片的模长差异产生偏差。我在实际测试中还发现一个细节图片预处理时很多教程用Resize(224)直接强制缩放但这会让非正方形的图片产生畸变影响特征质量。更好的做法是Resize(256)后CenterCrop(224)虽然牺牲了一点边缘信息但保持了宽高比提取出的特征更稳定。3. 图片重复检测的完整实现3.1 批量特征提取的工程化处理单张图片的特征提取逻辑确定后真正的工程难点是如何高效处理几万甚至几十万张图片。我最初写了个简单的循环一张一张地提取特征跑一万张图片花了一个多小时后来优化到十几分钟核心改动有三处。第一用DataLoader做批量推断。PyTorch的DataLoader可以一次性喂32张图给GPUbatch推理比单张推理快好几倍。这里要注意worker数量不要超过CPU核心数否则进程切换的开销会抵消并行收益。第二用SQLite做特征向量的持久化存储。很多人会把特征向量序列化成npy文件或pickle但一旦图片数量超过两万条全量加载到内存会占用几个GB。我把结构化信息文件路径、大小、宽高、修改时间存在SQLite表里特征向量用np.float32转成bytes直接存BLOB字段使用时按需分批加载。import sqlite3 import numpy as np def create_database(db_path): conn sqlite3.connect(db_path) conn.execute(CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE, file_size INTEGER, width INTEGER, height INTEGER, category TEXT, feature BLOB )) conn.execute(CREATE INDEX IF NOT EXISTS idx_path ON images(path)) conn.commit() return conn def insert_feature(conn, path, file_size, w, h, feature): # feature: np.ndarray float32, shape (2048,) blob feature.astype(np.float32).tobytes() conn.execute( INSERT OR REPLACE INTO images (path, file_size, width, height, feature) VALUES (?, ?, ?, ?, ?), (path, file_size, w, h, blob) )第三对超大图先做尺寸判断。有些摄影原图是6000x4000直接Resize到256再CenterCrop中间要解码一个24MB的JPEG耗时是普通图片的三到五倍。实际上对这种大图先按比例缩放到短边不超过1024像素再走预处理流程特征质量几乎没有损失但解码速度能提升一倍以上。3.2 相似度计算与阈值选择特征提取完成后重复检测就变成了一个向量检索问题。最直接的办法是双重循环计算两两之间的余弦相似度但5000张图片就是1250万次点积运算在Python里跑要等半天。我选择了faiss这个向量检索库没有GPU也能用CPU版本它能在一个小时内完成百万级别的两两比对。import faiss import numpy as np def build_index(features_matrix): # features_matrix: shape (N, 2048), float32, 已经L2归一化 d features_matrix.shape[1] index faiss.IndexFlatIP(d) # 内积 余弦相似度因为向量已归一化 index.add(features_matrix) return index def find_duplicates(index, features_matrix, threshold0.92): N features_matrix.shape[0] # 返回每个查询向量的topK近邻 similarities, neighbors index.search(features_matrix, k5) duplicate_pairs [] for i in range(N): for j in range(1, 5): # 跳过自己距离为1.0的那个 if similarities[i][j] threshold: pair (int(i), int(neighbors[i][j])) if pair[0] pair[1]: # 避免重复记录 duplicate_pairs.append(pair) return duplicate_pairs阈值的选择是这套系统里最需要花心思的地方。我拿了一个50000张真实个人照片库做测试统计不同阈值下的误报数和漏报数阈值检出重复组数误报占比漏报情况0.801832约18%漏报极少0.851420约9%少量漏报0.901062约3%有一些漏报0.92873约1.5%漏报明显增加0.95421约0.3%漏报较多最终我把默认阈值定在了0.90并支持可配置。原因是重复检测这个场景里把两张略微不同的图合并到一起的代价远小于把真正的重复漏掉导致硬盘继续吃紧的代价。略微调低阈值会带来一些误报但误报组可以通过后面的人审列表来过滤而漏报则是彻底找不回来了。另外要提醒一句不同主干网络提取的特征分布范围不一样EfficientNet的特征向量普遍比ResNet50更紧凑同样0.90的阈值在EfficientNet下会宽松一些。所以切换模型后最好重新跑一小批人工标注的数据来校准阈值别直接沿用旧参数。3.3 批量去重的工程化处理拿到重复对之后下一步是把散落的两两重复合并成一组重复文件。这里用并查集Union-Find最方便把所有互为重复的图片ID合并到同一个集合中每个集合就是一组重复文件。class UnionFind: def __init__(self, n): self.parent list(range(n)) def find(self, x): while self.parent[x] ! x: self.parent[x] self.parent[self.parent[x]] x self.parent[x] return x def union(self, x, y): rx, ry self.find(x), self.find(y) if rx ! ry: self.parent[ry] rx def group_duplicates(num_images, duplicate_pairs): uf UnionFind(num_images) for i, j in duplicate_pairs: uf.union(i, j) groups {} for idx in range(num_images): root uf.find(idx) groups.setdefault(root, []).append(idx) # 只保留包含至少2个成员的分组 return [members for members in groups.values() if len(members) 1]这里有个工程细节值得注意faiss返回的近邻列表里如果一对重复图片的相似度刚好卡在阈值边缘可能会出现A-B重复但B-C不重复的传递性关系最终导致某个大分组里混入一张搭便车的图。我在分组后加了一道校验对每个组内的所有成员做一次两两全量相似度检查如果某张图与组内其他所有图的相似度都低于阈值就把它踢出去。这一步增加了计算量但能显著降低误合并率。4. 视频重复检测的关键帧采样策略4.1 关键帧提取方案视频的重复检测比图片复杂一个量级因为你面对的不是一张图而是成百上千帧。核心思路是先抽帧再把每帧当作图片走CNN特征提取流程最后按视频级别聚合帧级相似度。帧采样策略直接决定检测效果。我在早期版本里用均匀抽帧每5秒抽一帧结果遇到同一视频一个带片头片尾、一个没有的情况就翻车了——因为均匀抽帧会把大量计算浪费在高度相似的连续帧上反而错过了真正有区分度的场景切换点。后来我换成了基于直方图的镜头边界检测来做关键帧提取核心逻辑是计算每帧的HSV颜色直方图如果当前帧与前一帧的直方图差异超过阈值就认为进入了新的镜头保留这一帧作为关键帧。这样做的好处是一部20分钟的视频可能只提取出5到8个关键帧但每个关键帧都代表了一个独特的视觉场景覆盖度远高于均匀抽帧。import cv2 import numpy as np def extract_keyframes(video_path, similarity_threshold0.7): cap cv2.VideoCapture(video_path) keyframes [] prev_hist None while True: ret, frame cap.read() if not ret: break # 缩小到统一尺寸降低直方图计算成本 small cv2.resize(frame, (160, 90)) hsv cv2.cvtColor(small, cv2.COLOR_BGR2HSV) hist cv2.calcHist([hsv], [0, 1], None, [16, 16], [0, 180, 0, 256]) hist cv2.normalize(hist, hist).flatten() if prev_hist is None: keyframes.append(frame) else: # 用相关系数衡量直方图相似度 corr cv2.compareHist(prev_hist, hist, cv2.HISTCMP_CORREL) if corr similarity_threshold: keyframes.append(frame) prev_hist hist cap.release() return keyframes这里用HISTCMP_CORREL而不是简单的差值是因为相关系数对光照变化有更好的鲁棒性。similarity_threshold0.7表示当前帧与上一帧只有70%相似就认为是新场景这个值是我拿几段不同类型的视频电影、Vlog、监控录像试出来的动态内容多的视频建议放宽到0.6静态内容多的可以收紧到0.8减少冗余关键帧。4.2 视频对相似度的聚合判断提取出两个视频各自的关键帧集合后需要定义这两个视频是否重复。最直观的方法是对A视频的每个关键帧在B视频的关键帧里找它的最大相似度再反过来对B的每个关键帧找A里的最大相似度最后计算双向平均相似度。如果双向平均相似度都超过阈值判定为重复。我用了一个更稳的方案取双向重叠比例。先为每个关键帧找到对方集合里最相似的帧并记录相似度然后统计相似度超过0.88的帧对占全部关键帧的比例。如果这个比例超过0.75就认为两个视频是重复的。def video_pair_similarity(feats_a, feats_b): feats_a, feats_b: 关键帧对应的特征向量列表 # 双向匹配 matched_pairs [] for i, fa in enumerate(feats_a): sims [np.dot(fa, fb) for fb in feats_b] j int(np.argmax(sims)) matched_pairs.append((i, j, sims[j])) # 统计匹配成功比例双向都要看 count_a sum(1 for _, _, s in matched_pairs if s 0.88) / len(feats_a) matched_pairs_b [] for j, fb in enumerate(feats_b): sims [np.dot(fa, fb) for fa in feats_a] i int(np.argmax(sims)) matched_pairs_b.append((i, j, sims[j])) count_b sum(1 for _, _, s in matched_pairs_b if s 0.88) / len(feats_b) return min(count_a, count_b)为什么要用到双向比例而不是简单地取平均因为我们遇到过一种情况A视频有20个关键帧B视频只有4个关键帧B是A的某个片段剪辑。如果只看A到B的匹配B只有4帧覆盖率低反过来B到A的匹配也低。双向比例取最小值能有效过滤这类包含关系而不是复制关系的情况。真正的重复视频不管从哪个方向去看关键帧的覆盖率都应该很高。4.3 转码、裁剪、加字幕场景的应对视频领域的重复形态比图片更复杂我用真实测试覆盖了几种典型场景记录一下效果场景说明检测效果同源重压同一视频被格式工厂压成不同码率稳定检出相似度常年在0.96以上裁剪加水印剪掉两侧黑边并加了台标稳定检出约0.93变速重录1.2倍速播放录制屏幕检出率下降约0.82需要调低阈值片段剪辑原视频的某一段单独成文件双向覆盖率不足无法检出加片头片尾重新封装加了5秒黑场和片尾logo可检出关键帧匹配对覆盖了中间内容这里最大的坑是变速重录场景。CNN特征本身对内容识别很强不会因为画面快了20%就认不出来但帧采样频率会变——原视频在1秒处有一帧作为关键帧重录的视频因为速度快这一秒的关键帧位置可能对不上。关键帧数量少的时候一对一的最近邻匹配就会失效。我的处理办法是对特征向量序列做时间轴的动态时间规整DTW匹配而不只是一对一最近邻。DTW允许原视频的第3帧匹配重录视频的第5帧在时间偏移场景下鲁棒性要好很多。代价是计算量大了些但我们的场景是本地个人库几万对视频两两做DTW匹配完全扛得住。5. 本地文件整理从检测结果到自动归档5.1 重复分组的聚类策略到这里我们已经能把重复文件归成组了。但真正做硬盘整理时还有一个问题有些文件虽然不是完全重复但内容上高度相近。典型的场景是一个摄影活动你连拍了10张RAW或者一个视频素材出了多个剪辑微调的版本每对的相似度都在0.85到0.95之间单独两两判断够不上重复阈值但整体放在一起一眼就能看出是同一个拍摄主题。为了解决这个问题我在重复检测之上加了一层主题聚类。做法很简单先按0.90阈值找出强重复组对剩下的图片用DBSCAN做密度聚类距离度量使用1减去余弦相似度eps设为0.15对应相似度阈值0.85min_samples设为2。这样能找出弱重复但明显同主题的图片簇方便用户决定是全部保留还是只留一张。需要注意的是DBSCAN的eps对结果非常敏感。我跑过一轮测试把eps从0.12调到0.18聚类组数几乎翻倍。建议用小批量预览调参或者把参数暴露给用户而不是写死。5.2 保留文件的选择规则每个重复组里该保留哪份文件这个决定不能拍脑袋否则可能误杀高质量原图。我设计了一套带优先级的打分规则按顺序决定组内最高优先级文件文件格式优先级无损格式TIFF、RAW、PNG、BMP优先于有损格式JPEG、WebP分辨率优先级宽高分辨率更高的优先文件大小优先级体积更大的优先通常表示码率更高或画质更好修改时间优先级更早的优先个人照片通常原图最早后生成的都是压缩副本def score_file(path, size, width, height, mtime): ext path.rsplit(., 1)[-1].lower() if ext in (tiff, arw, cr2, nef, png, bmp): format_score 100 elif ext in (jpg, jpeg, webp): format_score 60 else: format_score 40 resolution_score width * height # 归一化 resolution_score min(resolution_score / 1000000, 10) # 1MP得1分上限10分 size_score size / (1024 * 1024) # 每MB得1分上限20分 size_score min(size_score, 20) # 旧文件得分更高 import time age_score max(0, 10 - (time.time() - mtime) / (3600 * 24 * 365 * 2)) # 2年内线性递减 return format_score * 3 resolution_score * 8 size_score * 4 age_score * 5这个打分规则看起来复杂实际跑下来的效果比我最初用的无脑保留分辨率最高要好很多。因为有些分辨率高的文件其实是扫描件或者截图放大过的伪高清结合格式和时间的综合打分能更客观地找出真正的源头文件。5.3 整理目录结构与安全删除机制自动整理最忌讳的事情是直接删文件。我见过不止一个脚本因为路径拼接错误把整个目录删空。所以我实现的整理逻辑分成了三个阶段第一阶段软链接。在目标目录下创建整理后的目录结构但文件本身不移动只用软链接指向原始位置。这样即使规则设置错了也只是链接失效原始文件完好无损。第二阶段移动待定文件。用户确认分组无误后才把非保留文件移动到一个专门的duplicates_pending_review目录与原目录隔离。这相当于一个回收站机制文件只是被挪走了没有真正删除。第三阶段彻底清理。等用户人工确认这些待定文件确实都是重复副本后再执行物理删除操作。删除前会再次校验文件哈希和路径信息并生成一份删除清单供用户留档。这套机制保证了整个整理过程是可逆的我自己的实际用法是第一阶段跑完先放着过两周再手动检查一次分组结果然后才执行第二、三阶段。时间上的隔断能避免当天删完当晚后悔的惨剧。6. 实测表现与性能优化6.1 各类重复场景的实测结果工具完整跑通后我在三个不同规模的数据集上做了压测。结果如下数据集图片/视频数量原始体积检出重复数清理后体积准确率个人手机照片库12847张图片86GB1437组51GB检出准确率约97%家庭视频存档643个视频210GB89组重复147GB检出准确率约94%混合素材库38200个文件1.2TB5216组698GB检出准确率约95.5%个人手机照片库里最典型的就是微信传输产生的副本原图4MB微信压缩版1.2MB还有一个被截图软件裁剪过的1920x1080版本。这三个文件用pHash几乎是全网不同的水平但CNN特征的相似度都在0.96以上全部正确识别。视频库里表现最惊艳的是同一部纪录片被不同字幕组压制的情况这些视频的片头Logo、字幕字体、色彩饱和度都有明显差异人眼看得出不完全一样但内容层面确实是同一部片子。这套工具成功把它们归到了一组用户可以选择留清晰度最高的一版。6.2 推理速度与内存优化性能方面我做了几个关键优化实测提升非常明显批量尺寸调整。最初用batch_size32跑GPU推理单张平均8ms试了batch_size128后单张平均降到6ms。但超过128后提升不再明显反而显存占用飙到接近8GB。最终建议GPU显存6GB用648GB用128更多也没必要。CPU推理的多线程。没有独显的环境下PyTorch默认用了所有核但效果有个瓶颈。实测把torch.set_num_threads(8)配合batch_size16比默认配置快了大概40%。原因是过大的batch在CPU上会因为内存带宽不够反而降低吞吐。特征向量的存储压缩。2048维float32向量每条占8KB10万条就是800MB。压缩到float16后占用减半但实测对相似度计算精度影响极小误差在0.001以内。我在SQLite存储时改为float16序列化读取时再转回float32做计算内存和磁盘占用都降下来了。faiss索引的批量检索。前面提到用index.search(matrix, k5)一次检索全部N个特征向量比循环单次检索快了两个数量级。这里的小技巧是k别设太大5就够用。因为重复检测只需要找到一组里最强的几个重复就够了k5配合分组校验完全够用还能省下搜索时间。6.3 工程落地中的常见坑最后总结几个我在开发和测试过程中踩过的坑这些坑网上资料很少希望能帮后来者省点时间。第一个坑EXIF方向信息导致误判。手机拍的竖图很多.jpg文件的EXIF里存着orientation6需要旋转90度但OpenCV直接读图不会应用这个旋转信息。两张一模一样的照片一张旋转过一张没旋转特征相似度可能只有0.8出头。解决方法是读取时用PIL或者手动解析EXIF并应用旋转再做特征提取。我在预处理阶段加了这个逻辑之后竖屏照片的误判率直接降了一个数量级。第二个坑长视频抽帧的内存泄漏。早期版本用OpenCV逐帧读取视频时cap.read()返回的frame对象如果不显式释放在处理一部90分钟的电影时会越积越多最终内存涨到十几个GB。后来我在每处理完一个镜头后主动调用cv2.imencode把帧压缩成JPEG再存到临时目录处理完一个视频统一删掉内存泄漏问题才彻底解决。第三个坑自定义权重和torchvision版本的兼容性。我最初用自己训练微调过的ResNet50替换预训练模型结果加载torchvision自带权重时结构对不上程序直接报错。排查了半天发现是weights参数的枚举值从IMAGENET1K_V1到IMAGENET1K_V2在新旧版本torchvision里行为不一致。建议统一锁死torchvision的版本号并且加载权重前先打印模型的key名称做校验能省下很多诡异的报错时间。第四个坑中文路径的编码问题。本地整理工具面对的几乎都是中文文件名。在Windows上用Python处理含中文路径的文件时如果使用os.path和默认编码很容易遇到UnicodeDecodeError。统一用pathlib.Path代替字符串路径并在读取数据库时用UTF-8显式指定编码就不会有这个问题。7. 最后再分享几个我自己一直在用的操作习惯这工具跑通之后我整理了自己和身边几个朋友共十几块硬盘前前后后处理了超过十万个文件。整个过程下来有几个操作习惯我想特别记一笔算是给读到这里的你一些实际建议。第一个习惯是每个月固定跑一次增量检测。重复文件的产生是个持续过程——你把手机照片导进电脑、从聊天软件里保存附件、下载了别人二次压缩的视频都在不断制造重复。全量重跑太慢也没必要我写了个增量模式只对新入库的文件计算特征向量然后只拿新向量去和全库做相似度检索。实际测试下来每天新增100张图片的增量检测耗时不到30秒。第二个习惯是保留一份特征数据库的备份。SQLite库文件本身只有几百MB但它记录了整个文件库的特征指纹信息。一旦误删了原始文件只要特征库还在就能根据特征向量去别的备份设备上反向定位缺失的文件。我经历过一次误删事故靠这个特征库找回了三张当时以为彻底丢掉的旧照片。第三个习惯是整理归整理别动原始归档目录。我的目录设计是原始照片永远按自己的方式躺在原处整理工具只负责在_organized目录下生成软链接和重复分组报告。这样即使哪天发现某个分组判断错了直接删掉软链接目录就行原始文件一条都损失不了。这套基于CNN特征提取的重复检测方案的代码量其实不大核心逻辑加起来不超过一千行。但它解决的问题——在真实场景下识别看起来不完全一样但内容重复的图片和视频——是传统哈希方案根本做不到的。如果你也有硬盘越来越满、照片视频堆积成山的烦恼不妨照着我上面的思路自己搭一套或者直接拿公开的特征提取模型跑通这个流程。技术本身不难难的是过程中的细节打磨而我相信上面这些踩坑记录能帮你少走很多弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →