2

引言

最近需要用到这样的一个功能,但是搜索了一下网上的方法,对于不依赖神经网络而且简单易行的方法估计只有帧间差分了,网上的帧间差分代码好像在我的Pycharm上跑不起来,莫得办法只能自己手写一个了,之前没有接触过cv方面的代码,所以这算一次入门的机会吧,也希望源码能够帮助到大家。

代码的效率也许并不是最高的,尤其是关键的两个循环还是可以优化的,在这个博文里主要是想向大家分享一下思路,也希望能和大家讨论相关的内容吧。

如果急用,也可以直接跳到后面,会附上完整代码链接和使用方法。

思路

  • 导入视频文件
  • 逐帧处理
  • 差分
  • 差分值列表平滑
  • 窗口内寻找峰值
  • 切取视频关键帧

操作

准备工作

构造函数

我们需要定义这样一个类KeyFrameGetter,然后将可能用到的参数写进构造函数:


    def __init__(self, video_path, img_path, window=25):
        '''
        Define the param in model.
        :param video_path: The path points to the movie data,str
        :param img_path: The path we save the image,str
        :param window: The comparing domain which decide how wide the peek serve.
        '''
        self.window = window  # 关键帧数量
        self.video_path = video_path  # 视频路径
        self.img_path = img_path  # 图像存放路径
        self.diff = []  # 差分值的list
        self.idx = []  # 选取帧的list

其中这个window是后面选择峰值的一个窗口值,在后面的时候会解释

导入视频文件

接下来我们需要导入视频文件,我们事先相当于已经把视频路径放在了__init__里了,所以可以直接调用:

    def load_diff_between_frm(self, smooth=True, alpha=0.07):
        '''
        Calculate and get the model param
        :param smooth: Decide if you want to smooth the difference.
        :param alpha: Difference factor
        :return:
        '''
        print("load_diff_between_frm")
        cap = cv2.VideoCapture(self.video_path)  # 打开视频文件
        diff = []
        frm = 0
        pre_image = np.array([])
        curr_image = np.array([])

        while True:
            frm = frm + 1
            success, data = cap.read()
            if not success:
                break
            #  这里写接下来处理的函数体
  • cap = cv2.VideoCapture(self.video_path) 是调用cv2库直接打开视频文件,视频文件的路径就是self.video_path
  • diff是计算差分存储差分值的矩阵
  • frm是记录循环轮数的变量
  • 为了计算差分,分别使用pre_imagecurr_image记录前后两个图片

while True开始就是正式对每一帧进行遍历,success, data = cap.read()是读取每一帧,而等到读到最后一帧之后success就会返回False,这个时候就会退出循环。

逐帧处理

关键点就在每一帧要怎么对它处理:
首先:

            if frm == 1:
                pre_image = data
                curr_image = data
            else:
                pre_image = curr_image
                curr_image = data

如果是第一帧,那么没法差分,所以前后都存的是自己的数据;如果是后面的帧,那么到下一轮的时候把curr_image存到pre_image
接下来,就要进行图像处理和差分了:

            diff.append(abs_diff(pre_image, curr_image))

这一句中abs_diff就是计算差分值,这一个函数我们还没有写,我们现在就去定义这个函数:

def precess_image(image):
    '''
    Graying and GaussianBlur
    :param image: The image matrix,np.array
    :return: The processed image matrix,np.array
    '''
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 灰度化
    gray_image = cv2.GaussianBlur(gray_image, (3, 3), 0)  # 高斯滤波
    return gray_image


def abs_diff(pre_image, curr_image):
    '''
    Calculate absolute difference between pre_image and curr_image
    :param pre_image:The image in past frame,np.array
    :param curr_image:The image in current frame,np.array
    :return:
    '''
    gray_pre_image = precess_image(pre_image)
    gray_curr_image = precess_image(curr_image)
    diff = cv2.absdiff(gray_pre_image, gray_curr_image)
    res, diff = cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    #  fixme:这里先写成简单加和的形式
    cnt_diff = np.sum(np.sum(diff))
    return cnt_diff

思路是:

  • 分别预处理前后两个图像

    • 灰度化
    • 高斯滤波
  • 计算绝对值差分值
  • 进行二值化
  • 把矩阵内所有的数值加起来,作为差分值

讨论:
这样做法可能有点粗暴,因为相当于把整个矩阵压缩成了一维,但是我们只是为了衡量帧与帧之间的差距,所以这种信息损失并不是一个很大的问题。

差分值列表平滑

在主体循环结束后:

        if smooth:
            diff = exponential_smoothing(alpha, diff)
        #  标准化数据
        self.diff = np.array(diff)
        mean = np.mean(self.diff)
        dev = np.std(self.diff)
        self.diff = (self.diff-mean)/dev

        #  在内部完成
        self.pick_idx()

其中smooth是一个布尔型参数,也就是传入是否进行平滑的一个参数,当然我们这里要把平滑的代码写好hhh,不能偷懒,因为平滑之后选择的峰值更有代表性(会去掉一些干扰性很大的毛刺),然后我们要对数据进行标准化,最后调用self.pick_idx()这个函数,选取关键帧并保存。
指数平滑我参考了一下别人的代码,并进行了一定的改进:

def exponential_smoothing(alpha, s):
    '''
    Primary exponential smoothing
    :param alpha:  Smoothing factor,num
    :param s:      List of data,list
    :return:       List of data after smoothing,list
    '''
    s_temp = [s[0]]
    print(s_temp)
    for i in range(1, len(s), 1):
        s_temp.append(alpha * s[i - 1] + (1 - alpha) * s_temp[i - 1])
    return s_temp

它满足的是:
$y_{t+1}^{\prime}=y_{t}^{\prime}+\alpha\cdot \left(y_{t}-y_{t}^{\prime}\right)$
其中$\alpha$就是函数传入的参数alpha

窗口内寻找峰值

假设我要找一些关键帧,我需要找其中差分值较大的那部分,一种方法就是找所有差分值之间的前最大$k$个,其中$k$就是输入的要求值,但是这样的话高峰值旁边的一些值就会算到里面去,会导致相似的场景被重复取到。
解决方法:

  • 把一阶差分变成二阶差分(这样恢复index比较麻烦,而且DDL逼迫,所以放弃了这种方法。
  • 使用窗口内去找最大值的方法,这种方法的复杂度和二阶差分的差不多,所以我使用了这种方法:
    def pick_idx(self):
        '''
        Get the index which accord to the frame we want(peek in the window)
        :return:
        '''
        print("pick_idx")
        for i, d in enumerate(self.diff):
            ub = len(self.diff)-1
            lb = 0
            if not i-self.window//2 < lb:
                lb = i-self.window//2
            if not i+self.window//2 > ub:
                ub = i+self.window//2

            comp_window = self.diff[lb:ub]
            if d >= max(comp_window):
                self.idx.append(i)

        tmp = np.array(self.idx)
        tmp = tmp + 1  # to make up the gap when diff
        self.idx = tmp.tolist()
        print("Extract the Frame Index:"+str(self.idx))

提取关键帧

这一步就是非常机械的部分了,只要把收集好的self.idx对应取关键帧即可,我还没有在网上找到直接用$O(1)$的方式提取关键帧的方法,现在只能遍历所有的帧,然后把index in self.idx的帧找出来保存,如果有知道怎么直接按index取帧的朋友,希望能指点一下我。

    def save_key_frame(self):
        '''
        Save the key frame image
        :return:
        '''
        print("save_key_frame")
        cap = cv2.VideoCapture(self.video_path)  # 打开视频文件
        frm = 0
        idx = set(self.idx)
        while True:
            frm = frm + 1
            success, data = cap.read()
            if not success:
                break
            if frm in idx:
                print('Extracting idx:'+str(frm))
                cv2.imwrite(self.img_path+'/' + str(frm) + ".jpg", data)
                idx.remove(frm)
            if not idx:
                print('Done!')
                break

画出这个趋势图

我想知道差分是一个什么情况,然后我取了哪些点,所以写了这样的一段代码,注意其中$+1$是为了还原index

    def plot_diff_time(self):
        '''
        Plot the distribution of the difference along to the frame increasing.
        :return:
        '''
        plt.plot(self.diff, '-b')
        plt.plot(np.array(self.idx)-1, [self.diff[i] for i in self.idx], 'or')
        plt.xlabel('Frame Pair Index')
        plt.ylabel('Difference')
        plt.legend(['Each Frame', 'Extract Frame'])
        plt.title("The Difference for Each Pair of Frame")
        plt.plot()
        plt.show()

给大家看一下效果:
Figure_1.png

实现Demo

这里就是实现的Demo其中第一行是创建这个对象,第一个参数是视频的路径,第二个参数是存下关键帧的路径,第三个是窗口大小(单位是帧)。

第二行是获取模型的参数,也就是直接计算差分,参数就是一阶差分的$\alpha$。

第三行就是保存关键帧图片,最后就是把我想画的趋势图画出来。

if __name__ == '__main__':
    kfg = KeyFrameGetter('video/pikachu.mp4', 'img', 75)

    kfg.load_diff_between_frm(alpha=0.07)  # 获取模型参数
    kfg.save_key_frame()  # 存下index列表里对应图片

    kfg.plot_diff_time()

完整代码

Github关键帧提取项目


喵喵狂吠
6 声望5 粉丝

假发消费者,计科小学生。