前言:在使用WallpaperEngine制作视频桌面时,我之前总在想,我要怎么样才能搞到那种无缝循环的视频。首先想到的当然是用视频剪辑软件去剪,但感觉比较麻烦,还费时间。然后想到的是用一种类似的方法,只不过不用剪辑软件,而是直接使用截取视频的软件,而截取的时间点通过手动与一张视频关键帧截图进行比对来确定(将关键帧图片以半透明形式覆盖在原视频上)。这种也比较麻烦,但是速度还是比用剪辑软件快些。
为了解决上面方法的关键点——麻烦,速度慢。我决定用代码来完成这些步骤,一步到位。
这里主要有三步:
- 找到视频的完美循环的时间点(前提是有)。
- 视频预览(需要预览足够流畅,在循环点处不能卡)。
- 预览满意后,可以快速截取。
找到循环点
找循环点这块,我直接就想到了强大的ffmpeg库。它不仅功能强大(转码,截取,播放……),而且支持的视频格式也非常全,很多的热门播放器都是基于这个库开发的。思路是:用ffmpeg截取第一帧图像-》依次截取从第二帧开始的图像与第一帧进行相似度的匹配-》当相似度突破某个零界点时,判定为找到完美的循环点-》输出完美循环的起始位置。
提取帧和帧匹配直接在python中完成,并在预览播放器里调用:
import ffmpeg
def read_frame_as_jpeg(in_file, frame_num):
"""
指定帧数读取任意帧
"""
out, err = (
ffmpeg.input(in_file,loglevel='quiet')
.filter('select', 'gte(n,{})'.format(frame_num))
.output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
.run(capture_stdout=True)
)
return out
返回的就是截取的图片的byte[]数据。
相似度检测关键在于匹配的算法,网上查了一些资料,貌似“cosin相似度”比较靠谱,不过相对来说比较慢(为了以最快的速度找到匹配点,我开启了多个线程同时从不同的位置开始匹配)。下面是相关算法的代码:
# 对图片进行统一化处理
def get_thum(image, size=(64, 64), greyscale=False):
# 利用image对图像大小重新设置, Image.ANTIALIAS为高质量的
image = image.resize(size, Image.ANTIALIAS)
if greyscale:
# 将图片转换为L模式,其为灰度图,其每个像素用8个bit表示
image = image.convert('L')
return image
# 计算图片的余弦距离
def image_similarity_vectors_via_numpy(image1, image2):
image1 = get_thum(image1)
image2 = get_thum(image2)
images = [image1, image2]
vectors = []
norms = []
for image in images:
vector = []
for pixel_tuple in image.getdata():
vector.append(average(pixel_tuple))
vectors.append(vector)
# linalg=linear(线性)+algebra(代数),norm则表示范数
# 求图片的范数??
norms.append(linalg.norm(vector, 2))
a, b = vectors
a_norm, b_norm = norms
# dot返回的是点积,对二维数组(矩阵)进行计算
res = dot(a / a_norm, b / b_norm)
return res
按下面的逻辑完成匹配,并返回时间点就行了。注意这里在没有找到完全匹配的帧时,依然会返回相似度最高的帧(在一些场合能用到的)。
sim_value = image_similarity_vectors_via_numpy(img1, img2)
print('---------index: ' + str(frame) + ' 相似度: ' + str(sim_value))
#记录所有关键帧中匹配度最高的帧
if(sim_value >= most_same_most_similar_info['simvalue']):
most_same_most_similar_info['simvalue'] = sim_value
most_same_most_similar_info['frame_index'] = frame
#相似度超过了阈值,匹配完成
if sim_value >= threhold_sim_value:
print('找到了符合的关键帧 end frame: ' + str(frame))
end_frame = frame + extra_offset
isfinded = True
#写入找到的信息
p_start_time = start_frame / total_frame * total_duration
p_end_time = end_frame / total_frame * total_duration
p_len_time = p_end_time - p_start_time
print('匹配成功,循环起止时间:%s ~ %s 起止帧:%s ~ %s 总时长:%s 相似度:%s' %
(p_start_time, p_end_time,start_frame,end_frame, p_len_time, sim_value))
save_loop_info(p_start_time,p_end_time,file_path)
print('准备退出...')
效果的预览
这里开始本来打算直接使用ffmpeg来写个自定义播放器的。不过后来感觉自己水平拉跨,可能最终做不出自己想要的效果,所以转战了Unity。使用了AVProVideoPro这个插件,用起来真不错,效率也很棒,播放非常流畅。在Seek了位置后也不卡顿。最终效果如图:
预览播放器采用极简的界面,基本都是通过快捷键操作。这个播放器集成了一般的视频截取工具的基础功能。还提供一键截取用于对照的关键帧(以半透明方式盖在视频画面上,方便手动微调,精确匹配)
视频截取方面
截取还是使用ffmpeg来完成。比较简单,直接调用相关命令行就行了,这里直接通过预览播放器来调用(预览时可能对时间点进行调整)。值得一提的是,截取的模式有两种,一种是copy模式(速度极快,无损,但时间不准),另一种是转码模式(速度较慢,有损,时间较准确),我这里两种都包括了。
private void CutCurrentVideo()
{
string save_name = GetCutVideoSaveDir("mp4");
if (!string.IsNullOrEmpty(save_name))
{
float beginSec = currentEditInfo.startTime * 0.001f;
float endSec = currentEditInfo.endTime * 0.001f;
string cmdStr = "";
if (appconfig.isZhuanma)
{
cmdStr = string.Format(@" -y -ss {0} -t {1} -i ""{2}"" -c:v libx264 -c:a aac -strict experimental -b:a 640k ""{3}"""
, beginSec, endSec - beginSec, currentEditInfo.path, save_name);
}
else
{
cmdStr = string.Format(@" -y -accurate_seek -ss {0} -t {1} -i ""{2}"" -acodec copy -vcodec copy -async 1 -avoid_negative_ts 1 ""{3}"""
, beginSec, endSec - beginSec, currentEditInfo.path, save_name);
}
string ffmpeg_exepath = "ffmpeg.exe";
Utils.RunCmd(ffmpeg_exepath, cmdStr);
}
}
到这里流程就完成了。
另外附上gitee的源码:
jiuyueqiji123/VideoPefectLoopCuttergitee.com