思路很简单:
这里md5,为了加速计算,没有算文件的完整md5。(之前看到过这种算法,忘了在哪里看来的,大概是用于上传文件时,快速判断是否与已有文件对比验证用的)将文件分成256块,每块取前8个字节计算md5,这样能快速计算出一个大概可以用于判断文件唯一性的md5。
完整代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import time
import hashlib
def main():
path = 'd:/'
fp_arr = file_search(path,repat=r'.*\.mp4') # 查找文件(文件类型自行填写,不写查所有文件类型)
du_arr = find_duplicate_file(fp_arr) # 检查重复
# [fp_arr.remove(l) for j in [i[1:] for i in du_arr] for l in j] # 去重,重复文件只保留第1个即可
def file_search(path='.',repat = r'.*'):
"""
文件查找:
文件夹及子文件夹下,所有匹配文件,返回list文件列表,绝对路径形式
Args:
path: 文件路径(默认当前路径)
repat: 文件名正则匹配,不区分大小写(默认匹配所有文件)
return: 文件列表(绝对路径)
Returns:
files_match: 文件列表
"""
# 获取文件夹,及子文件夹下所有文件,并转为绝对路径
folders,files = [],[]
st = time.time()
repat = '^'+repat+'$'
# walk结果形式 [(path:文件夹,[dirlist:该文件夹下的文件夹],[filelist:该文件夹下的文件]),(子文件夹1,[子子文件夹],[]),(子文件夹2,[],[])...]
# 该遍历会走遍所有子文件夹,返回上述形式的结果信息。
for record in os.walk(path):
fop = record[0]
folders.append(fop)
for fip in record[2]:
fip = os.path.abspath(os.path.join(fop,fip)).replace('\\','/')
files.append(fip)
# 逐个检查是否符合要求
files_match = []
for file in files:
a = re.findall(repat,file.lower())
if a:
files_match+=a
print('找到{0}个文件'.format(len(files_match)))
# 返回满足要求的
return files_match
def fastmd5(file_path,split_piece=256,get_front_bytes=8):
"""
快速计算一个用于区分文件的md5(非全文件计算,是将文件分成s段后,取每段前d字节,合并后计算md5,以加快计算速度)
Args:
file_path: 文件路径
split_piece: 分割块数
get_front_bytes: 每块取前多少字节
"""
size = os.path.getsize(file_path) # 取文件大小
block = size//split_piece # 每块大小
h = hashlib.md5()
# 计算md5
if size < split_piece*get_front_bytes:
# 小于能分割提取大小的直接计算整个文件md5
with open(file_path, 'rb') as f:
h.update(f.read())
else:
# 否则分割计算
with open(file_path, 'rb') as f:
index = 0
for i in range(split_piece):
f.seek(index)
h.update(f.read(get_front_bytes))
index+=block
return h.hexdigest()
def find_duplicate_file(fp_arr):
"""
查找重复文件
Args:
fp_arr:文件列表
"""
# 将文件大小和路径整理到字典中
d = {} # 临时词典 {文件大小1:[文件路径1,文件路径2,……], 文件大小2:[文件路径1,文件路径2,……], ……}
for fp in fp_arr:
size = os.path.getsize(fp)
d[size]=d.get(size,[])+[fp]
# 列出相同大小的文件列表
l = [] # 临时列表 [[文件路径1,文件路径2,……], [文件路径1,文件路径2,……], ……]
for k in d:
if len(d[k])>1:
l.append(d[k])
# 核对大小一致的文件,md5是否相同
ll = [] # 临时列表 [[文件路径1,文件路径2,……], [文件路径1,文件路径2,……], ……]
for f_arr in l:
d = {} # 临时词典 {文件大小1:[文件路径1,文件路径2,……], 文件大小2:[文件路径1,文件路径2,……], ……}
for f in f_arr:
fmd5 = fastmd5(f)
d[fmd5]=d.get(fmd5,[])+[f]
# 找到相同md5的文件
for k in d: # 相同大小的文件,核对一下md5是否一致
if len(d[k])>1:
ll.append(d[k])
print('查重完毕,发现{0}处重复'.format(len(ll)))
for i in ll:
print(i)
return ll
if __name__ == '__main__':
main()
思路:对视频进行抽帧,然后比对是否有关键帧的图片指纹是否一致
这里写一下研究过程,实现代码:
这个过程试过一些方案也都记录一下:
曾经考虑subprocess.Popen()执行ffmpeg抽帧,但是太慢了
def external_cmd(cmd, msg_in=''):
# 将subprocess.call(cmd)包装了一下,这样就能获取到执行cmd命令时,产生的输出内容了。
try:
proc = subprocess.Popen(cmd,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout_value, stderr_value = proc.communicate(msg_in)
return stdout_value, stderr_value
except ValueError as err:
# log("ValueError: %s" % err)
return None, None
except IOError as err:
# log("IOError: %s" % err)
return None, None
'''方法一'''
# 1秒抽0.05帧,也就是20s抽1帧,1420s长度视频抽73镇,耗时94s
external_cmd('ffmpeg -i "{0}" -r 0.05 -q:v 2 -f image2 ./%08d.000000.jpg'.format(video_path))
'''方法二'''
# 20s抽1帧,1420s长度视频抽70帧,耗时18s
timeF = 20
for i in range(1,video_duration//timeF):
h,m,s = (i*timeF)//3600, ((i*timeF)%3600)//60, (i*timeF)%60
external_cmd('ffmpeg -i "{0}" -ss {1:0=2}:{2:0=2}:{3:0=2} -vframes 1 {4}.jpg'.format(video_path,h,m,s,i)) # 抽取指定时间点起的第一帧
'''方法三'''
# 20s抽1帧,1420s长度视频抽70帧,并压缩到100*100耗时17s(对图像的压缩处理基本不影响速度,时间开销的大头也不是出在文件存储上,而是ffmpeg定位时间为位置然后抽帧本身就慢)
timeF = 20
for i in range(1,video_duration//timeF):
h,m,s = (i*timeF)//3600, ((i*timeF)%3600)//60, (i*timeF)%60
hw = '{0}x{0}'.format(100)
external_cmd('ffmpeg -i "{0}" -ss {1:0=2}:{2:0=2}:{3:0=2} -vframes 1 -s {5} -f image2 {4}.jpeg'.format(video_path,h,m,s,i,hw))
最后选定的还是cv2抽帧
这个是一开始想的,将抽到的帧保存为单张图像,发现还是慢。
'''
# 视频抽帧测试,这种抽帧方式太慢了,1000帧大概45秒长度视频,花费5秒左右
videopath = '01.mp4'
vc = cv2.cv2.VideoCapture(videopath)
if vc.isOpened(): # 是否正常打开
rval,frame = vc.read()
else:
rval = False
timeF =1000 # 抽帧频率
c = 1
while rval:
rval,frame = vc.read()
if(c%timeF==0):
cv2.imwrite('{0:0=3}.jpg'.format(c),frame)
cv2.waitKey(1)
c+=1
vc.release()
'''
然后换成了这种,不存图像了,直接将抽到图像计算成dhash保存,总算速度上来了。
# 视频,取指定时间点图片,转指定宽高后,计算图像指纹
v = 'c:/users/kindle/desktop/test/01.mp4'
cap = cv2.VideoCapture(v) #打开视频文件
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) #视频的帧数
fps = cap.get(cv2.CAP_PROP_FPS) #视频的帧率
dur = n_frames / fps #视频的时间
cap.set(cv2.CAP_PROP_POS_MSEC, (5*1000)) # 跳到指定时间点,单位毫秒
success, image_np = cap.read() # 返回该时间点的,图像(numpy数组),及读取是否成功
img = Image.fromarray(cv2.cvtColor(image_np,cv2.COLOR_BGR2RGB)) # 转成图像格式
imgrsz = img.resize((100,100)) # 缩放到指定宽高(后来发现是否缩放基本不影响)
# imgrsz.save('5.jpg') # 保存图像
# imgrsz.show() # 显示图像
计算图像指纹,直接用了现成的模块,imagehash里的dhash
h5 = str(imagehash.dhash(imgrsz)) # 生成图像指纹
在上述基础上,视频转换为图像指纹组的函数基本如下
def video2imageprint(filepath):
"""
返回整个视频的图片指纹列表
从3秒开始,每60秒抽帧,计算一张图像指纹
"""
cap = cv2.VideoCapture(filepath) ##打开视频文件
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) #视频的帧数
fps = cap.get(cv2.CAP_PROP_FPS) #视频的帧率
dur = n_frames / fps *1000 #视频大致总长度
cap_set = 3000
hash_int_arr = []
while cap_set<dur-3000: # 从3秒开始,每60秒抽帧,计算图像指纹。总长度-3s,是因为有的时候计算出来的长度不准。
cap.set(cv2.CAP_PROP_POS_MSEC, cap_set)
# 返回该时间点的,图像(numpy数组),及读取是否成功
success, image_np = cap.read()
if success:
img = Image.fromarray(cv2.cvtColor(image_np,cv2.COLOR_BGR2RGB)) # 转成cv图像格式
h = str(imagehash.dhash(img))
hash_arr.append(h) # 图像指纹
else:
print('fail',cap_set/1000,filepath)
cap_set+=1000*60
cap.release() # 释放视频
return hash_arr
然后将建立字典,key为图像指纹,value为地址列表。
# shelve用来做python的字典型数据库,并将其存储在磁盘上。
# shelve的key要求必须是字符串,value则可以是任意合法的python数据类型
db = shelve.open('videocheck.db')
# 写入数据库
for h in hash_arr:
fp_arr = db.get(h, []) # 具有相同指纹的对应的视频路径列表
if fp_arr==[]:
db[h]=[filepath]
elif filepath not in fp_arr:
db[h]=db[h]+[filepath]
db.close()
后面就是检查哪个指纹,对应的地址列表中,大于1个文件。
则说明有多个视频包含该指纹。
为了验证指纹相同的图像是否一致,还写了一个合并图像输出的函数。这个函数写成了这样,是考虑以后可以用作给视频生成多图合并的缩略图玩。
def imgjoin(imgs,tags=[],width_height=(0,0),column_row=(0,0),blank=(0,0,0,0,0,0)):
'''
多张图片,合并成一张视频抽帧缩略图合并大图那种。
可以每张图片上方加注释,也可以文件顶部只加一行注释。
每张图片宽高,行列间距,四外边距都可以自定义
args:
imgs: pil图片数组
tags: 如果标签数和图片数相同,每张图片上方加文字。如果只有一个标签,则只在图片最顶部加1条文字。
width_height: 合并后图片中,每张缩略图宽高,如未指定以第一张图标为基准
column_row:横排和竖排数量
blank_cr:空白分布(列间,行间,左右,上下,标题,标签)
return: 返回合并好的图片
'''
from PIL import Image,ImageDraw,ImageFont
# 检查是否符合规则
if len(imgs)>100:
print('imgs当前上限100张图合并')
return ''
elif imgs==[]:
print('imgs中没有包含图片,请检查')
return ''
elif 1<len(tags)<len(imgs):
print('tags文字数组和图片对不上,请只输入1条或和图片一样多')
return ''
else:
pass
# 每行每列个数
if column_row==(0,0):
cr = 1
while len(imgs)>cr**2:
cr+=1
column_row=(cr,cr)
c,r = column_row
# 调整每张图片到指定宽高,如未指定,以第一张图片宽高为基准:
if width_height==(0,0):
width_height = imgs[0].size
for i,m in enumerate(imgs):
if m.size!=width_height:
imgs[i] = m.resize(width_height) # 缩放到指定宽高
w,h = width_height
# 空白分布
bw,bh,blr,btb,btitle,btag = blank # (列间,行间,左右,上下,标题,标签)
if blank==(0,0,0,0,0,0):
if len(tags)==1:
btitle = h
# 生成输出图像尺寸
J_width = w*c + bw*(c-1) + blr*2 # 总计图像宽度+列间距+左右边距
J_height= h*r + bh*(r-1) + btb*2 + btitle + btag*r # 总计图像高度+行间距+顶底边距+标题高度+标签高度
J_img = Image.new('RGB', (J_width,J_height),(255,255,255))
draw=ImageDraw.Draw(J_img)
newfont=ImageFont.truetype('simkai.ttf',12)
# 合并图像
for i,m in enumerate(imgs):
if i==0: # 第一张图
x,y=blr,btb+btitle+btag # 第一张图左上角位置
elif i%c==0: # 新的一行
x,y=blr,y+bh+btag+h
else:
x,y=x+bw+w,y
J_img.paste(m, (x, y, x+w, y+h))
# 添加文字
if len(tags)>1:
draw.text((x,y-btag),tags[i],(0,0,0),font=newfont)
return J_img
到这里最开始的研究就完成了,
最开始的实现思路,就是上面这样。
================
后来发现图像指纹是有可能不是完全一致的,
而是相似的,还要考虑到相似的图像指纹。
imagehash.dhash算出来的图像指纹,本身的type类型不是字符串。
为了保存,转为字符串后,后续计算两个字符串的相似度,哪怕是很简单的字符串每一位是否与另一字符串每一位相等,数以10w个图像指纹,互相计算都要花费很长时间。
计算两个指纹的相似度,试了几种方法效率,最后发现bin最快,这个方法还是从dhash的官网看来的。
2020-5-13 看到还有一种写法是
num = 1 - (aHash - bHash)/len(aHash.hash)**2
直接imagehash计算,速度和bin的差不多,推荐使用这个。
'''关于dhash相似度比较方法研究,得到bin的方法计算最快,我的家用电脑10w次大概0.057秒。'''
import time
a = 'a1a8739f324eb01c'
b = 'a1a8749f323eb01c'
ai = int(str(a),16)
bi = int(str(b),16)
st = time.time()
# 10w次执行速度,bin方式最快
for i in range(100000):
# num = [a[j] is b[j] for j in range(16)].count(True)/16 # 0.2097s
# num = [a[j] == b[j] for j in range(16)].count(True)/16 # 0.2082s
# num = difflib.SequenceMatcher(None, a,b).ratio() # 4.2250s
num = 1-bin(ai^bi).count("1")/64 # 0.0568s
et = time.time()
print(num,et-st)
接下来的考虑思路就是
1秒比对200w个感觉是挺快
但是1000个,长度为1小时的视频,就需要30分钟比对完。
这个计算量感觉太大,即使写出来,为提高效率可能需要其他算法之类的优化。
关于效率处理这里,并没有完全想好,也没有时间测试,暂时就搁置了。
因为是个人闲暇研究,扔了可能后续就忘了,捡不起来了。
所以这里把之前的研究过程记录一下,希望其他有用到的人能得到一些参考。