视频镜头分割

最近学习视频和图像的处理,刚好要先将视频进行预处理。因为需要针对不同的镜头来做不一样的处理,所以在做进一步处理之前,要将整段视频进行分割处理。
先是在网上搜索了一下,找到一篇可以借鉴的文章 《视频镜头分割与关键帧提取》。里面把整个算法思路都说的很清楚,虽然没有具体的代码实现,这个不重要,思路比代码更重要,按照对应的思路,通过python写出了具体的实现代码,可是最后在我处理的视频上的效果不是很明显,所以暂时将这个方案搁置。
后来在github找到了一个关于如何为视频生成简介的项目,项目也是没有太多的实现,给了一篇国外论文的地址。花了三天时间总算把那篇难啃的英文论文看懂了,整理了一下里面的思路,然后将之前的思路结合起来,就写出了我自己的实现代码。
一、边缘检测

根据帧图像的灰度值直方图差异进行边缘检测,差异值越大的帧可能就是镜头边缘帧。这种方式可以避免在镜头移动或者图像中出现动态移动的时候差异,提高边缘检测的准确性。其中要注意的地方
1、相邻的两个镜头,中间的帧图像个数应该有一个阈值,也就是说帧数相差太少不认可为新的一个镜头
2、检测出来的镜头边缘帧,它与前一帧的差值应该是此镜头中,所有帧差中最大的。其的值也应该是当前镜头中所有帧差均值的一个倍数

二、具体算法
1、创建类,用来存储每一帧的信息。

# 由于我处理的视频一个就近2G,为了减少内存的消耗,所以不会再内存中存放帧的数据信息,只计算需要的数据后释放掉
class frame_info:
    def __init__(self, index, diff):
        self.index = index  # 帧编号
        self.diff = diff  # 当前帧与前一帧的diff

2、创建函数,用来计算帧间差值。

           gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  
           n_pixel = frame.shape[0] * frame.shape[1] 
           hist = cv2.calcHist([gray], [0], None, [16], [0, 256])  
           hist = hist * (1.0 / n_pixel)
           diff = np.sum(np.abs(np.subtract(hist, pre_hist)))

这就是第一步遍历视频,计算整个视频中所有帧的帧差。存放到一个列表中,第一帧的帧差默认是0。
接下来就是对于边缘帧的检测

3、找寻差值最大的帧,这里需要简单说明一下算法思路

1、创建一个窗口,定义窗口中帧的数量,每次对窗口中的帧进行判断。然后取对应数量的帧;
2、计算窗口中差值最大的帧,定义为可疑的镜头边缘帧M再进行下一步判断;
3、取得前一镜头边缘帧P,判断当前M与P中间的帧数量,是否超过设定的镜头最小帧数阈值,如果不超过,则舍弃M,清空窗口数据,进行下一个镜头判断;否则进行下一步判断;
4、判断M的差值是不是P到M的平均差值(不包括M的差值)的一个阈值倍数。

def second_find_diff_max(list_frames = [], start_no = 0):
    sus_max_frame = []  # 可疑的镜头帧,以M为值
    window_frame = []

    length = len(list_frames)
    index_list = range(0, length)
    for index in index_list:
        frame_item = list_frames[index]
        window_frame.append(frame_item)

        if len(window_frame) < window_size:
            continue

        # 处理窗口帧的判断
        max_diff_frame = getMaxFrame(window_frame)
        max_diff_index = max_diff_frame.index

        if len(sus_max_frame) == 0:
            sus_max_frame.append(max_diff_frame)
            continue
        last_max_frame = sus_max_frame[-1]

        '''
            判断是否超过镜头跨度最小值
            1、低于,则移除窗口中最大帧之前的所有帧(包括最大帧),然后重新移动窗口
            2、则进入下一步判断
        '''
        if (max_diff_index - last_max_frame.index) < m_MinLengthOfShot:
            start_index = window_frame[0].index
            if last_max_frame.diff < max_diff_frame.diff:
                #  最后一条可疑frame失效
                sus_max_frame.pop(-1)
                sus_max_frame.append(max_diff_frame)
                pop_count = max_diff_index - start_index + 1
            else:
                #  舍弃当前的可疑frame,整个窗口清除
                pop_count = window_size

            count = 0
            while True:
                window_frame.pop(0)
                count += 1
                if count >= pop_count:
                    break
            continue

        '''
            镜头差超过最小镜头值后的下一步判断,判断是否为可疑帧
            当前最大帧距离上一个可疑帧的平均差值是否差距很大
        '''
        sum_start_index = last_max_frame.index + 1 - start_no
        sum_end_index = max_diff_index - 1 - start_no
        id_no = sum_start_index
        # print("{0}, {1}, {2}".format(sum_start_index, sum_end_index, id_no))
        sum_diff = 0
        while True:

            sum_frame_item = list_frames[id_no]
            sum_diff += sum_frame_item.diff
            id_no += 1
            if id_no > sum_end_index:
                break

        average_diff = sum_diff / (sum_end_index - sum_start_index + 1)
        if max_diff_frame.diff >= (m_suddenJudge * average_diff):
            sus_max_frame.append(max_diff_frame)

        window_frame = []
        continue

    sus_last_frame = sus_max_frame[-1]
    last_frame = list_frames[-1]
    if sus_last_frame.index < last_frame.index:
        sus_max_frame.append(last_frame)

    return sus_max_frame

4、在上一步的处理后,其实效果已经可以实现部分了。但是在实际的测试中,发现效果还是有一定的问题,所以做了一个简单的优化步骤

具体的优化处理思路就是,处理在上面中在连续帧差值都比较波动大的时候,很容易出现镜头帧获取错误的问题。还有就是陡增、陡降的时候,镜头边缘帧的判断失误问题。
整体的思路就是:
1、如果当前可疑帧是陡增情况,即其前面的多个帧的差值都很低,突然其却很高,形成了一个近乎近90度的陡增效果,则认为其为镜头边缘帧
2、陡降的效果,类似于陡增处理
3、当前找到的可疑边缘帧,其帧差值应该是其附近(前后)一定帧数范围内的最大值;

def third_optimize_frame(tag_frames, all_frames, start_no):
    '''
        进一步优化
        对于每一个分割镜头帧,其前后的帧的平均值都远远低于其
    '''
    new_tag_frames = []
    for tag_frame in tag_frames:

        tag_index = tag_frame.index

        if tag_frame.diff < m_diff_threshold:
            continue

        #  向前取m_MinLengthOfShot个帧
        pre_start_index = tag_index - m_offset_frame_count - m_offset
        pre_start_no = pre_start_index - start_no
        if pre_start_no < 0:
            #  如果往前找时已经到头了,则认为此镜头不可取,将镜头交给最起始的帧
            new_tag_frames.append(all_frames[0])
            continue
        pre_end_no = tag_index - 1 - start_no - m_offset

        pre_sum_diff = 0
        emulator_no = pre_start_no
        while True:
            pre_frame_info = all_frames[emulator_no]
            pre_sum_diff += pre_frame_info.diff
            emulator_no += 1
            if tag_frame.index == 42230:
                print("向前:{0}, {1}".format(pre_frame_info.index, pre_frame_info.diff))
            if emulator_no > pre_end_no:
                break

        #  向后取m_MinLengthOfShot个帧
        back_end_index = tag_index + m_offset_frame_count + m_offset
        back_end_no = back_end_index - start_no
        if back_end_no >= len(all_frames):
            #  如果往后找时已经到头了,则认为此镜头不可取,将镜头交给结束的帧
            new_tag_frames.append(all_frames[-1])
            continue
        back_start_no = tag_index + 1 - start_no + m_offset

        back_sum_diff = 0
        emulator_no = back_start_no
        while True:
            back_frame_info = all_frames[emulator_no]
            back_sum_diff += back_frame_info.diff
            emulator_no += 1
            if emulator_no > back_end_no:
                break

        is_steep = False
        # 判断是不是陡增/或者陡降
        pre_average_diff = pre_sum_diff / m_offset_frame_count
        print("前平均 {0}, {1}, {2}".format(tag_frame.index, tag_frame.diff, pre_average_diff))
        if tag_frame.diff > (m_optimize_steep * pre_average_diff):
            is_steep = True

        back_average_diff = back_sum_diff / m_offset_frame_count
        print("后平均 {0}, {1}, {2}".format(tag_frame.index, tag_frame.diff, back_average_diff))
        if tag_frame.diff > (m_optimize_steep * back_average_diff):
            is_steep = True

        # 计算平均值,如果大于一定的阈值倍数,则认可,不然舍弃
        sum_diff = pre_sum_diff + back_sum_diff
        average_diff = sum_diff / (m_offset_frame_count * 2)
        print("{0}, {1}, {2}".format(tag_frame.index, tag_frame.diff, average_diff))
        if tag_frame.diff > (m_optimize * average_diff) or is_steep:
            new_tag_frames.append(tag_frame)

    return new_tag_frames

这样就得到了所有镜头帧的编号,然后对应视频找镜头帧编号就行了。我用了视频进行测试,得到的效果还是很满意的。


测试结果图片

其中33-0表示:截下来的视频镜头总数量——有问题的镜头数量
这样整体的结果就是:230——10,而且其中的错误镜头还有是因为视频花屏导致的。所以最后的成功率基本在90%以上,基本满足了需求。

如果有疑问或者有更好的建议,我们可以一起探讨,共同进步!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 个人为了看的方便没有广告,转载自泡泡机器人:https://www.sohu.com/a/161346283_71...
    Maxsium阅读 38,900评论 0 8
  • 五月,天逐渐热了,我的心却慌了。六月的高考在不断靠近,家里的气氛也逐渐紧张,就在这时身体一向健壮的姥爷却病了。家里...
    欣_宇阅读 277评论 0 0
  • 如果一件事让我们苦苦纠结,那么还是放下心中那份计算,跟随本心去做就好了。 心之所向的事情往往都是自然而然,不需要左...
    Ada岳慧阅读 287评论 0 0
  • 从小我就喜欢和比我大的人玩,因为可以得到义务性的照顾和免费的人生经验。而最近我认识了一批比我小的朋友,其中有一个可...
    muuul阅读 617评论 0 0
  • 前言 为什么会有视觉笔记、手帐、插画这些混淆点在大家的脑海中呢? 刚接触视觉化表达的小伙伴一定都有过这样的疑惑,视...
    明霞菇凉阅读 2,115评论 0 2