使用帧相似度匹配编写无缝循环视频截取工具

 

前言:在使用WallpaperEngine制作视频桌面时,我之前总在想,我要怎么样才能搞到那种无缝循环的视频。首先想到的当然是用视频剪辑软件去剪,但感觉比较麻烦,还费时间。然后想到的是用一种类似的方法,只不过不用剪辑软件,而是直接使用截取视频的软件,而截取的时间点通过手动与一张视频关键帧截图进行比对来确定(将关键帧图片以半透明形式覆盖在原视频上)。这种也比较麻烦,但是速度还是比用剪辑软件快些。

为了解决上面方法的关键点——麻烦,速度慢。我决定用代码来完成这些步骤,一步到位。

这里主要有三步:

  1. 找到视频的完美循环的时间点(前提是有)。
  2. 视频预览(需要预览足够流畅,在循环点处不能卡)。
  3. 预览满意后,可以快速截取。

找到循环点

找循环点这块,我直接就想到了强大的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/VideoPefectLoopCutter​gitee.com

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值