Python音视频开发:消除抖音短视频Logo和去电视台标

☞ ░ 前往老猿Python博文目录

一、引言

对于带Logo(如抖音Logo、电视台标)的视频,有三种方案进行Logo消除:

  1. 直接将对应区域用对应图像替换;
  2. 直接将对应区域模糊化;
  3. 通过变换将要去除部分进行填充。

其中:
方法1又可以使用三种方法,一是使用某固定图像替换、二是截取视频某帧的一部分图像替换、三是用每帧固定区域的图像替换当前帧的Logo区域,其中固定图像替换最简单,下面就不展开介绍;截取视频某帧的一部分图像比较简单,用每帧固定区域的图像替换当前帧的Logo区域最复杂;

方法2可以认为是方法3的特例,即填充值来源于简单计算,如Logo区域像素的均值等,我们在此不进行介绍。

方法3是以Logo去除后根据原Logo区域附近的图像像素对Logo区域进行插值填充,以确保填充后的图像整体比较协调、完整。

二、需要解决的问题

  1. 怎么确认Logo区域?当然是使用鼠标选择确认Logo区域最方便;
  2. 使用图像去替换Logo区域时,在鼠标选择过程中怎么确保替换图像大小与被替换图像大小一致?这个需有将替换图像进行裁剪或填充;
  3. 通过变换将要去除部分进行填充时,怎么确保填充值与整体视频比较协调?本文采用根据Logo邻近像素进行插值填充
  4. 对于抖音这种在晃动的Logo怎么修复?老猿采用多次取样Logo区域来修复。

三、背景知识

3.1、OpenCV视频预览方法

可以通过cv2.imshow(winname, img)来显示一个图片,当读取视频文件的帧图片连续显示时就是一个无声的视频播放。其中的参数winname为一个英文字符串,显示为窗口的标题,OpenCV将其作为窗口的名字,作为识别窗口的标识,相同名字的窗口就是同一个窗口。

对于相关窗口,OpenCV提供鼠标及键盘事件处理机制。

3.2、OpenCV-Python的鼠标事件捕获

OpenCV提供了设置鼠标事件回调函数来提供鼠标事件处理的机制,设置回调函数的方法如下:
cv2.setMouseCallback(winName, OnMouseFunction, param)
其中winName为要设置鼠标回调处理的窗口名,OnMouseFunction为回调函数,用于处理鼠标响应,param为设置回调函数时传入的应用相关特定参数,可以不设置,但需要在回调函数访问设置回调函数对象属性时非常有用。

3.3、OpenCV的几何图形绘制

OpenCV提供了在图像中绘制几何图形的方法,绘制的图像包括矩形、椭圆、扇形、弧等。本文主要介绍矩形的绘制,具体调用语法如下:

rectangle(img, pt1, pt2, color, thickness=None, lineType=None, shift=None)

其中参数:

  • img:要显示的图像,为numpy数组,格式为BGR格式
  • pt1:左上角点的坐标
  • pt2:右下角点的坐标
  • color:绘制的颜色,为BGR格式的三元组,如(255,0,0)表示蓝色
  • thickness:边框的厚度,如果为负数,则该矩形为实心矩形,否则为空心矩形
  • linetype:线型,包括4连通、8连通以及抗锯齿线型,使用缺省值即可
  • shift:坐标值的精度,为2就表示精确到小数点后2位

另外该方法还有个变种调用方式:
rectangle(img, rec, color[, thickness[, lineType[, shift]]]),其中的rec为上面pt1和pt2构建的矩形。

3.4、Moviepy的视频变换方法

fl_image方法为moviepy音视频剪辑库提供的视频剪辑类VideoClip的视频变换方法,具体请参考《moviepy音视频剪辑:视频剪辑基类VideoClip的属性及方法详解》。

3.5、Python的全局变量传值

在python中可以使用全局变量,关于全局变量的使用请参考《 Python函数中的变量及作用域》的介绍。

3.6、OpenCV的图像修复方法

OpenCV中的cv2.inpaint()函数使用插值方法修复图像,调用语法如下:
dst = cv2.inpaint(src,mask, inpaintRadius,flags)
参数含义如下:

  • src:输入8位1通道或3通道图像
  • inpaintMask:修复掩码,8位1通道图像。非零像素表示需要修复的区域
  • dst:输出与src具有相同大小和类型的图像
  • inpaintRadius:算法考虑的每个点的圆形邻域的半径
  • flags:修复算法标记,其中INPAINT_NS表示基于Navier-Stokes方法,INPAINT_TELEA表示Alexandru Telea方法。具体方法在此不展开介绍

3.7、OpenCV的颜色空间转换方法

cv2.cvtColor是openCV提供的颜色空间转换函数,调用语法如下:
cvtColor(src, code, dstCn=None)
其中:

  • src:要转换的图像
  • code:转换代码,表示从何种类型的图像转换为何种类型,如下面需要使用的cv2.COLOR_BGR2GRAY就是将BGR格式彩色图像转换成灰度图片
  • dstCn:目标图像的通道数,如果为0表示根据源图像通道数以及转换代码自动确认

3.8、图像阈值处理

openCV图像的阈值处理又称为二值化,之所以称为二值化,是它可以将一幅图转换为感兴趣的部分(前景)和不感兴趣的部分(背景)。转换时,通常将某个值(即阈值)当作区分处理的标准,通常将超过阈值的像素作为前景。

阈值处理有2种方式,一种是固定阈值方式,又包括多种处理模式,另一种是非固定阈值,由程序根据算法以及给出的最大阈值计算图像合适的阈值,再用这个阈值进行二值化处理,非固定阈值处理时需要在固定阈值处理基础上叠加组合标记。

调用语法:
retval, dst = cv2.threshold (src, thresh, maxval, type)
其中:

  • src:源图像,8位或32位图像的numpy数组
  • thresh:阈值,0-255之间的数字,在进行处理时以阈值为边界来设不同的输出
  • maxval:最大阈值,当使用固定阈值方法时为指定阈值,当叠加标记时为允许最大的阈值,算法必须在小于该值范围内计算合适的阈值
  • type:处理方式,具体取值及含义如下:
    在这里插入图片描述
  • dst:阈值化处理后的结果图像numpy数组,其大小和通道数与源图像相同
  • retval:叠加cv2.THRESH_OTSU或cv2.THRESH_TRIANGLE标记后返回真正使用的阈值

案例:

ret, mask = cv2.threshold(img, 35, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)

补充说明:

  1. 阈值判断时,是以小于等于阈值和大于阈值作为分界条件
  2. 如果是32位彩色图像,则是以RGB每个通道的值单独与阈值进行比较,按每个通道进行阈值处理,返回的是一个阈值处理后的RGB各自的值

3.8、图像膨胀处理

关于膨胀处理的知识解释有点复杂,请参考《OpenCV-Python学习—形态学处理》以及《Opencv python 锚点anchor位置及borderValue的改变对膨胀腐蚀的影响》。
图像的膨胀处理会使得图像中较亮的区域增大,较暗的区域减小。

四、具体实现

本部分介绍的内容对Logo去除采用了如下四种方式:

  1. 使用视频中某帧图像的指定区域内容替换Logo
  2. 使用视频中每帧图像的指定区域内容替换当前帧的Logo区域
  3. Logo区域采用图像修复
  4. 多Logo区域采样图像修复

其中第四种方法是Logo区域的Logo在视频中为晃动的内容(如抖音的Logo)时需要,如果是静止不变的Logo用第三种方法就够了。

以上四种处理方式,对应的消除Logo方法类型分别为:

ridLogoManner_staticImg = 1
ridLogoManner_frameImg = 2
ridLogoManner_inpaint = 3
ridLogoManner_multiSampleInpaint = 4

4.1、实现思路

为了实现Logo标记的消除,具体步骤如下:

  1. 展现视频并设置鼠标回调函数;
  2. 识别鼠标动作用鼠标在视频图像中圈定Logo位置;
  3. 根据不同方法确认是否需要选择替换图像;
  4. 对视频中的每帧图像进行图像处理。

4.2、实现鼠标回调函数

这是一个比较通用的鼠标回调函数,代码如下:

def OnMouseEvent( event, x, y, flags, param):
    try:
        mouseEvent = param
        mouseEvent.processMouseEvent(event, x, y, flags)

    except Exception as e:
        print("使用回调函数OnMouseEvent的方法错误,所有使用该回调函数处理鼠标事件的对象,必须满足如下条件:")
        print("    1、必须将自身通过param传入")
        print("    2、必须定义一个processMouseEvent(self)方法来处理鼠标事件")
        print(e)

所有使用该回调函数处理鼠标事件的对象,必须将自身通过param传入到回调函数中,并且必须定义一个processMouseEvent(self)方法来处理鼠标事件。下面介绍的类CImgMouseEvent就是满足条件的类。

4.3、视频图像展现窗口的鼠标事件处理类

为了支持在视频图像中进行相关操作,需要比较方便的支持并识别鼠标操作的类,在此称为CImgMouseEvent, CImgMouseEvent用于OpenCV显示图像的窗口的鼠标事件处理,会记录下前一次鼠标左键按下或释放的位置以及操作类型,并记录下当前鼠标移动、左键按下或释放事件的信息。

4.3.1、CImgMouseEvent关键属性

  • mouseIsPressed:表示鼠标左键当前是否为按下状态
  • playPaused:表示当前窗口播放视频(就是连续显示视频的帧)是否暂停状态,当鼠标左键按下时,播放暂停,通过鼠标左键双击或右键点击继续播放
  • previousePos、pos:上次和本次鼠标事件的位置
  • previousEvent、event:上次和本次鼠标事件的类型
  • parent:为创建CImgMouseEvent对象的调用者,该对象必须定义一个processMouseEvent方法,用于当鼠标事件执行时的具体操作
  • winName:CImgMouseEvent处理鼠标事件所属的窗口名
  • img:在窗口中当前显示的图像对象,可以通过showImg显示图像并改变winName、img

以上鼠标事件属性的记录处理都在CImgMouseEvent的方法processMouseEvent中,但processMouseEvent方法仅记录鼠标事件属性,记录后调用父对象的 parent的processMouseEvent方法实现真正的操作

4.3.2、CImgMouseEvent主要方法

  • processMouseEvent:鼠标事件回调函数调用该方法记录鼠标事件数据,并由该方法调用父对象的processMouseEvent方法实现真正的操作
  • showImg:在窗口winName中显示img图像,并设置鼠标回调函数为OnMouseEvent
  • getMouseSelectRange:获取鼠标左键按下位置到当前鼠标移动位置或左键释放位置的对应的矩形以及矩形最后位置的鼠标事件类型,如果无都有操作则返回None
  • drawRect:画下当前鼠标事件选择的矩形或参数指定的矩形,一般供父对象调用
  • drawEllipse:画下当前鼠标事件选择的矩形或参数指定的矩形的内接椭圆,一般供父对象调用

4.3.3、 CImgMouseEvent类实现代码

class CImgMouseEvent():
    def __init__(self,parent,img=None,winName=None):
        self.img = img
        self.winName = winName
        self.parent = parent
        self.ignoreEvent = [cv2.EVENT_MBUTTONDOWN,cv2.EVENT_MBUTTONUP,cv2.EVENT_MBUTTONDBLCLK,cv2.EVENT_MOUSEWHEEL,cv2.EVENT_MOUSEHWHEEL] #需要忽略的鼠标事件
        self.needRecordEvent = [cv2.EVENT_MOUSEMOVE,cv2.EVENT_LBUTTONDOWN,cv2.EVENT_LBUTTONUP] #需要记录当前信息的鼠标事件
        self.windowCreated = False #窗口是否创建标记
        if img is not None:self.showImg(img,winName)
        self.open(winName)

    def open(self, winName=None):
    #初始化窗口相关属性,一般情况下此时窗口还未创建,因此鼠标回调函数设置不会执行
        if winName:
            if self.winName != winName:
                if self.winName:
                    cv2.destroyWindow(self.winName)
                    self.windowCreated = False
                self.WinName = winName

        self.mouseIsPressed = self.playPaused = False
        self.previousePos = self.pos = self.previousEvent = self.event = self.flags = self.previouseFlags = None
        if self.winName and self.windowCreated : cv2.setMouseCallback(self.winName, OnMouseEvent, self)

    def showImg(self,img,winName=None):
        """
        在窗口winName中显示img图像,并设置鼠标回调函数为OnMouseEvent
        """
        if not winName:winName = self.winName
        self.img = img
        if winName != self.winName:
            self.winName = winName
            self.open()

        if not self.windowCreated:
            self.windowCreated = True
            cv2.namedWindow(winName)#cv2.WINDOW_NORMAL| cv2.WINDOW_KEEPRATIO | cv2.WINDOW_GUI_EXPANDED
            cv2.setMouseCallback(winName, OnMouseEvent, self)
        cv2.imshow(winName, img)

    def processMouseEvent(self,event, x, y, flags):
        #鼠标回调函数调用该函数处理鼠标事件,包括记录当前事件信息、判断是否记录上次鼠标事件信息、是否暂停视频播放,调用parent.processMouseEvent() 执行响应操作
        #mouseventDict = {cv2.EVENT_MOUSEMOVE:"鼠标移动中",cv2.EVENT_LBUTTONDOWN:"鼠标左键按下",cv2.EVENT_RBUTTONDOWN:"鼠标右键按下",cv2.EVENT_MBUTTONDOWN:"鼠标中键按下",cv2.EVENT_LBUTTONUP:"鼠标左键释放",cv2.EVENT_RBUTTONUP:"鼠标右键释放",cv2.EVENT_MBUTTONUP:"鼠标中键释放",cv2.EVENT_LBUTTONDBLCLK:"鼠标左键双击",cv2.EVENT_RBUTTONDBLCLK:"鼠标右键双击",cv2.EVENT_MBUTTONDBLCLK:"鼠标中键双击",cv2.EVENT_MOUSEWHEEL:"鼠标轮上下滚动",cv2.EVENT_MOUSEHWHEEL:"鼠标轮左右滚动"}
        #print(f"processMouseEvent {mouseventDict[event]} ")

        if event in self.ignoreEvent:return
        if self.event in [cv2.EVENT_LBUTTONDOWN,cv2.EVENT_LBUTTONUP]:#当上次鼠标事件左键按下或释放时,上次信息保存
            self.previousEvent,self.previousePos,self.previouseFlags  = self.event,self.pos,self.flags

        if  event==cv2.EVENT_LBUTTONUP:
            self.mouseIsPressed = False
        elif event == cv2.EVENT_LBUTTONDOWN:
            self.mouseIsPressed = True
            self.playPaused = True
        elif  event in [cv2.EVENT_LBUTTONDBLCLK,cv2.EVENT_RBUTTONDBLCLK,cv2.EVENT_RBUTTONDOWN,cv2.EVENT_RBUTTONUP]:#鼠标右键动作、鼠标双击动作恢复视频播放
            self.playPaused = False 
        if event in self.needRecordEvent:
            self.event,self.flags,self.pos = event,flags,(x,y)

        self.parent.processMouseEvent()  #调用者对象的鼠标处理方法执行

    def getMouseSelectRange(self):
        """
        获取鼠标左键按下位置到当前鼠标移动位置或左键释放位置的对应的矩形以及矩形最后位置的鼠标事件类型
        :return: 由鼠标左键按下开始到鼠标左键释放或鼠标当前移动位置的矩形,为None表示当前没有这样的操作
        """
        if self.previousEvent is None or self.event is None:
            return None
        if (self.event!=cv2.EVENT_LBUTTONUP)  and (self.event!=cv2.EVENT_MOUSEMOVE): #最近的事件不是鼠标左键释放或鼠标移动
            return None
        if self.pos == self.previousePos:#与上次比位置没有变化
            return None
        if (self.previousEvent== cv2.EVENT_LBUTTONDOWN ) and (self.event==cv2.EVENT_LBUTTONUP): #鼠标左键按下位置到鼠标左键释放位置
            return [self.previousePos,self.pos,cv2.EVENT_LBUTTONUP]
        elif (self.previousEvent== cv2.EVENT_LBUTTONDOWN ) and (self.event==cv2.EVENT_MOUSEMOVE):#鼠标左键按下位置到鼠标当前移动位置
            return [self.previousePos, self.pos, cv2.EVENT_MOUSEMOVE]
        return None

    def drawRect(self,color,specRect=None,filled=False):
        """
        :param color: 矩形颜色
        :param specRect: 不为None画specRect指定矩形,否则根据鼠标操作来判断
        :param filled: 是画实心还是空心矩形,缺省为空心矩形
        :return: 画下的矩形,specRect不为None时是specRect指定矩形,否则根据鼠标操作来判断
        """
        if specRect:
            rect = specRect
        else:
            rect = self.getMouseSelectRange()

        if rect:
            img = self.img
            img = self.img.copy()
            if not filled:
                cv2.rectangle(img, rect[0], rect[1], color,1)
            else:
                cv2.rectangle(img, rect[0], rect[1], color,-1)
            cv2.imshow(self.winName, img)
            return rect
        else:
            return None

    def drawEllipse(self, color,specRect=None, filled=False):
        """
        :param color: 椭圆颜色
        :param specRect: 不为None画specRect指定椭圆,否则根据鼠标操作来判断
        :param filled: 是画实心还是空心椭圆,缺省为空心椭圆
        :return: 画下的椭圆对应的外接矩形,specRect不为None时是specRect指定矩形,否则根据鼠标操作来判断
        """
        if specRect:
            rect = specRect
        else:
            rect = self.getMouseSelectRange()
        if rect:
            x0, y0 = rect[0]
            x1, y1 = rect[1]
            x = int((x0+x1)/2)
            y = int((y0+y1)/2)
            axes = (int(abs(x1-x0)/2),int(abs(y1-y0)/2))
            img = self.img.copy()
            if not filled:
                cv2.ellipse(img, (x, y),axes, 0,0,360,  color,1)
            else:
                cv2.ellipse(img, (x, y),axes, 0,0,360,  color,-1)
            cv2.imshow(self.winName, img)
            return rect
        else:
            return None

    def close(self):
        cv2.destroyWindow(self.winName)
        self.windowCreated = False

    def __del__(self):
        self.close()

4.4、定义视频图像处理类

CSubVideoImg类用于操作视频及视频的图像,主要用于对一个视频的帧图像进行操作。

4.4.1、CSubVideoImg关键属性

  • replaceObject:替换图对象, 类型为四元组,分别对应 replaceImg, replaceRect, targetReplaceImg, frame,用于前两种消除方法,存储选择的替换图像、替换图像区域矩形、按照Logo区域进行替换图像裁剪和填充后的静态替换图像、以及替换图像选择时所在的帧图像
  • logoObjectList:列表,1…n个元素(多次采样Logo区域图像时n大于1),每个元素是个二元组,每个二元组表示一个logo图像信息,包括图像的数组以及图像的位置及大小等信息,形如:[(logoImg1,logoRect1),…,(logoImgn,logoRectn)],除了第四种消除方法,前面三种处理方法都只取最后一个元素使用,即最后选择的Logo图像有效
  • frameMask:记录下Logo图像掩码的帧,该帧除了Logo图像对应的掩码内容外,其他部分全为0
  • multiFrameMask:多次采样的frameMask叠加

4.4.2、CSubVideoImg主要方法

  • processMouseEvent:响应鼠标事件的方法
  • drawSelectRange:画出当前鼠标左键选择的范围,目前可以画矩形或椭圆
  • setVideoClipRect:按指定帧率播放视频(仅图像),并提供在视频图像中选中某个矩形范围,并在接下来播放中一直显示该矩形,按EsC或q或Q退出
  • getROI:在setVideoClipRect基础上返回选中ROI图像、并显示该ROI图像,可以获取视频中的多个ROI区域,选定一个ROI区域后,按N、n、S、s保存当前选择区域,按退出键会保存最后一个区域
  • replaceImgRegionBySpecImg:将指定图像的指定位置的一个矩形图像替换为参数指定图像内容
  • replaceImgRegionBySpecRange:将指定图像的指定位置的一个矩形范围内的图像替换为该图像内另一个矩形矩形范围对应的内容
  • adjuestImgAccordingRefImg:将指定图像大小调整为参数指定的参考图像的大小,如果指定图像大小超出参考图像则对原图像进行裁剪,否则对指定图像进行扩充
  • createImgMask:生成一个图像的掩码图像,采用转换为灰度图像后再进行图像阈值处理、再进行膨胀处理后返回该处理后的图像
  • genLogoFrameMask:将Logo图像的掩码图像与视频帧大小的全0图像叠加后生成的帧掩码图像
  • genMultiLogoFrameMask:将多个Logo图像生成的帧掩码图像叠加生成的帧掩码图像
  • convertVideo:将消除Logo图像的视频输出
  • previewVideoByReplaceLogo:预览图像替换消除Logo的视频
  • previewVideoByInpaintLogo:预览图像修复术消除Logo的视频

4.4.3、CSubVideoImg类实现代码

class CSubVideoImg():
    def __init__(self,videoFName):
        super().__init__()
        self.imgMouseEvent = CImgMouseEvent(self)     #创建鼠标事件处理对象
        self.videoFName = videoFName
        self.exitKeys = [ord('q') ,ord('q'), 27] #视频图像播放时退出键定义,包括q、Q以及ESC
        self.initStatus()

    def initStatus(self):#初始化相关变量,self.rect为记录最后一个鼠标选择框
        self.rect = self.logoObjList = self.replaceObject = self.frameMask = None


    def processMouseEvent(self):#鼠标事件响应函数,将当前选择框显示出来
        self.drawSelectRange() 

    def drawSelectRange(self,specRect=None):
        if specRect:
            rect = self.imgMouseEvent.drawRect((255, 0, 0),specRect)
        else:
            rect = self.imgMouseEvent.drawRect((255, 0, 0))
            if rect: self.rect = rect

    def displayImg(self,winname,img,seconds):
        cv2.imshow(winname, img)
        ch = cv2.waitKey(seconds*1000)
        cv2.destroyWindow(winname)

    def getROI(self, operInfo, fps=24):
        """
       获取视频中的多个ROI区域(即鼠标选择区域),选定一个ROI区域后,按N、n、S、s保存当前选择区域
       按指定帧率播放视频(仅图像),并提供在视频图像中选中某个矩形范围,并在接下来播放中一直显示该矩形,按EsC或q或Q退出
       退出后会显示当前选择的ROI图像
       :param operInfo: 播放窗口提示信息,也即窗口名,必须是英文
       :param fps: 播放的帧率
       :return: 返回选择的ROI及对应帧的二元组,类似:([(rect1,img1),...,(rectn,imgn)],frame)矩形和最后选中操作所在帧的选择图像
        """
        frame = None
        cap = cv2.VideoCapture(self.videoFName)
        self.imgMouseEvent.open(operInfo)

        ROIList = []
        saveKeys = [ord('n'), ord('N'), ord('s'), ord('S')]+self.exitKeys #保存和退出键都保存最后一个选择矩阵范围

        self.rect = None
        if not cap.isOpened():
            print("Cannot open video")
            return None

        while True:
            if not self.imgMouseEvent.playPaused: #正在播放
                ret, frame = cap.read()
                if not ret:
                    if frame is None:
                        print("The video has end.")
                    else:
                        print("Read video error!")
                    break
                self.imgMouseEvent.showImg(frame, operInfo)
                self.drawSelectRange(self.rect)

            ch = cv2.waitKey(int(1000 / fps))
            if ch in saveKeys:
                if self.rect is not None:
                    x0, y0 = self.rect[0]
                    x1, y1 = self.rect[1]
                    ROI = frame[y0:y1, x0:x1]
                    ROIList.append((ROI, self.rect))
                    self.rect = None
            if ch in self.exitKeys: break
        # 完成所有操作后,释放捕获器
        if len(ROIList) == 0:
            self.imgMouseEvent.close()
            cap.release()
            return None,None

        self.imgMouseEvent.close()
        cap.release()

        return ROIList, frame

    def replaceImgRegionBySpecImg(self,srcImg,regionTopLeftPos,specImg):
        """
        将srcImg的regionTopLeftPos开始位置的一个矩形图像替换为specImg
        :return: True 成功,False失败
        """
        srcW, srcH = srcImg.shape[1::-1]
        refW, refH = specImg.shape[1::-1]
        x,y =  regionTopLeftPos
        if (refW>srcW) or (refH>srcH):
            #raise ValueError("specImg's size must less than srcImg")
            print(f"specImg's size {specImg.shape[1::-1]} must less than srcImg's size {srcImg.shape[1::-1]}")
            return False
        else:
            srcImg[y:y+refH,x:x+refW] = specImg
            return True

    def replaceImgRegionBySpecRange(self,srcImg,regionTopLeftPos,specRect):
        """
        将srcImg的regionTopLeftPos开始位置的一个矩形图像替换为srcImg内specRect指定的一个矩形范围图像
        :return: True 成功,False失败
        """
        srcW, srcH = srcImg.shape[1::-1]
        refW, refH = specRect[1][0]-specRect[0][0],specRect[1][1]-specRect[0][1]
        x,y =  regionTopLeftPos
        if (refW>srcW) or (refH>srcH):
            print(f"specImg's size {(refW, refH)} must less than srcImg's size {srcImg.shape[1::-1]}")
            return False
        else:
            srcImg[y:y+refH,x:x+refW] = srcImg[specRect[0][1]:specRect[1][1],specRect[0][0]:specRect[1][0]]
            return True

    def adjuestImgAccordingRefImg(self,img,refimg,color=None):
        """
        按照refimg大小调整img大小,如果是扩充,则采用img边缘像素的镜像复制或指定颜色创建扩充像素
        :param img:
        :param refimg:
        :param color:
        :return:
        """
        srcW,srcH = img.shape[1::-1]
        refW,refH = refimg.shape[1::-1]
        if srcW>refW:
            diff = int((srcW-refW)/2)
            img = img[:,diff:refW+diff]
        if srcH>refH:
            diff = int((srcH - refH) / 2)
            img =img[diff:refH+diff,:]
        srcW, srcH = img.shape[1::-1]
        w = max(srcW,refW)
        h = max(srcH,refH)
        diffW = int((w-srcW)/2)
        diffH = int((h-srcH)/2)
        if color is None:
            dest = cv2.copyMakeBorder(img,diffH,h-srcH-diffH,diffW,w-srcW-diffW,cv2.BORDER_REFLECT_101) #上下左右扩展当前图像
        else:
            dest = cv2.copyMakeBorder(img, diffH,h-srcH-diffH,diffW,w-srcW-diffW, cv2.BORDER_CONSTANT,color)#上下左右扩展当前图像,扩展部分颜色为color
        rectSize = (h,w)
        return dest

    def createImgMask(self, img):
        # 创建img的掩码
        img2gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, mask = cv2.threshold(img2gray, 35, 255, cv2.THRESH_BINARY) #转为像素值为0和255的二值图,阈值为35

        #对掩码进行膨胀处理
        element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        mask = cv2.dilate(mask, element)

        return mask

    def genLogoFrameMask(self,frame,logoObject):
    #将Logo掩码填充到一与视频帧大小相同的全0图像中
        logoImg,logoRect = logoObject
        if logoImg is  None:
            return None
        else:
            logMask = self.createImgMask(logoImg)
            frameMask = np.zeros(frame.shape[0]*frame.shape[1],dtype=np.uint8)
            frameMask = frameMask.reshape(frame.shape[0:2])
            x0,y0 = logoRect[0]
            x1,y1 = logoRect[1]
            frameMask[y0:y1,x0:x1] = logMask
            return frameMask

    def genMultiLogoFrameMask(self, logoObjectList,frame ):
    #将多次采样的Logo掩码填充到一与视频帧大小相同的全0图像中
        composeFrameMask = None
        for logoObject in logoObjectList:
            frameMask = self.genLogoFrameMask(frame, logoObject)
            if composeFrameMask is None:
                composeFrameMask = frameMask
            else:
                composeFrameMask = cv2.add(composeFrameMask, frameMask)

        return composeFrameMask

    def convertVideo(self,outPutFName,ridLogoManner,logoObjects,replaceObject=None,frameMask=None):
        #生成视频
        global videoImgConvertParams

        if ridLogoManner in [ridLogoManner_staticImg,ridLogoManner_frameImg]:
            if replaceObject is None:
                return False,"替换图像尚未提供,请先选择替换图像"
        else:
            if frameMask is None:
                return False,"替换frameMask尚未提供或未生成,请确保进行了Logo图像的截取操作,请先提供"

        self.frameMask = frameMask
        self.replaceObject = replaceObject
        self.logoObjList  = logoObjects
        self.ridLogoManner = ridLogoManner

        try:
            videoImgConvertParams = self, ridLogoManner
            clipVideo = VideoFileClip(self.videoFName)
            newclip = clipVideo.fl_image(processImg)
            newclip.write_videofile(outPutFName, threads=8)
            clipVideo.close()
            newclip.close()
        except Exception as e:
            return False,f"生成视频时出现异常:\n{e}"
        else:
            return True,f"视频处理完成,生成的视频保存着在文件:{outPutFName}"

    def previewVideoByReplaceLogo(self,fps,logoObjects,replaceObject,ridLogoManner):
        """
        使用替换区域或替换图像替换logo区域后的视频效果预览
        fps:fps
        用于使用静态图像或同帧图像替换后预览视频使用
        :param logoObjects:
         二元组:(logoObjectList,FRAME),实际形如([(logoImg1,logoRect1),...,(logoImgn,logoRectn)],FRAME)
         logoObjectList:列表,1...n个元素(只有当采用多次采样修复算法时才会n大于1),每个元素是个二元组,每个二元组表示一个logo图像信息,包括图像的数组以及图像的位置及大小等信息,
         形如:[(logoImg1,logoRect1),...,(logoImgn,logoRectn)]
         Frame:截取Logon图像的帧对应数组,当预览一个帧时可以使用

        :param replaceObject:四元组(replaceImg, replaceRect,targetReplaceImg frame)

        :param ridLogoManner:消除logo的方式

        :return:
        """
        global  videoImgConvertParams
        videoImgConvertParams = self,ridLogoManner

        self.frameMask = None
        self.replaceObject = replaceObject
        self.logoObjList = logoObjects
        self.ridLogoManner = ridLogoManner

        cap = cv2.VideoCapture(self.videoFName)

        if not cap.isOpened():
            print("Cannot open video")
            return
        winName = f"video previewing fps={fps}"
        while True:
            ret, frame = cap.read()
            if not ret:
                if frame is None:
                    print("The video has end.")
                else:
                    print("Read video error!")
                break

            frame = processImg(frame)

            cv2.imshow(winName, frame)

            ch = cv2.waitKey(int(1000 / fps))
            if ch in self.exitKeys:  break
        # 完成所有操作后,释放捕获器

        cap.release()
        cv2.destroyWindow(winName)

    def previewVideoByInpaintLogo(self,fps, logoObjects,frameMask, ridLogoManner):
        """
        使用图像修复术对logo区域处理后的视频效果预览
        fps:fps
        :param logoObjects:列表,1...n个元素(当多次采样Logo时n大于1),每个元素是个二元组,每个二元组表示一个logo图像信息,包括图像的数组以及图像的位置及大小等信息,
        形如:[(logoImg1,logoRect1),...,(logoImgn,logoRectn)]
        Frame:截取Logon图像的帧对应数组,当预览一个帧时可以使用

        :param ridLogoManner:消除logo的方式
      
        """
        global      videoImgConvertParams

        if ridLogoManner not in [ridLogoManner_inpaint, ridLogoManner_multiSampleInpaint]:
            print("ridLogoManner is not fit previewVideoByInpaintLogo ")
            return False


        videoImgConvertParams = self, ridLogoManner

        self.frameMask = None
        self.replaceObject = None
        self.logoObjList = logoObjects
        self.ridLogoManner = ridLogoManner

        winName = f"video previewing,fps={fps}"

        self.frameMask = frameMask
        self.multiFrameMask = frameMask
        cap = cv2.VideoCapture(self.videoFName)

        if not cap.isOpened():
            print("Cannot open video")
            return

        while True:
            ret, frame = cap.read()
            if not ret:
                if frame is None:
                    print("The video has end.")
                else:
                    print("Read video error!")
                break
            frame = processImg(frame)
            cv2.imshow(winName, frame)


            ch = cv2.waitKey(int(1000 / fps))

            if ch in self.exitKeys:  break
        # 完成所有操作后,释放捕获器

        cap.release()
        cv2.destroyWindow(winName)

上面相关定义的与视频预览、帧预览等方法定义时的参数包括了记录下完整Logo采用对象、替换对象、以及Logo掩码等,这些数据需要在操作视频图像时记录并在视频处理时传递给上述方法。

4.5、视频图像处理函数

上面视频图像处理类中使用了processImg函数,该函数用于视频生成的帧图像处理函数,用静态图像或同帧区域范围图像替换,或使用图像修复术修复。

在processImg函数中,使用了全局变量来传递该函数调用时的CSubVideoImg类对象及Logo消除的方式。具体实现就二十行代码,大家可以参考视频变换的介绍自己去实现,在此就不提供了,否则就和付费专栏文章完全一样了。

4.6、主程序

主程序根据Logo消除类型来显示视频执行Logo图像选择、替换图像选择(前2种Logo消除类型)后,将视频进行消除处理。

def main(ridLogoManner):
    videoOperation = CSubVideoImg(r"f:\video\mydream.mp4")
    destFName = r"f:\video\mydream_new_"+str(ridLogoManner)+".mp4"
    fps = 24
    replaceObject = logoObjList = multiFrameMask = frameMask = None


    print("请在播放的视频中选择要去除Logo的区域:")
    logobjs, frame = videoOperation.getROI("select multiLogo Imgs Range", fps)
    if logobjs is not None and len(logobjs):
        logoObjList = (logobjs, frame)
        frameMask = videoOperation.genMultiLogoFrameMask([logobjs[-1]], frame)
        multiFrameMask = videoOperation.genMultiLogoFrameMask(logobjs, frame)
        frame = frame
    else:
        print("本次操作没有选择对应Logo图像,程序退出。")
        return

    if ridLogoManner in ( ridLogoManner_staticImg, ridLogoManner_frameImg):  # ridLogoManner_inpaint , ridLogoManner_multiSampleInpaint
        print("请在播放的视频中选择要去除Logo的区域:")
        replaceObjList, frame = videoOperation.getROI("select Replace Img Range")
        if replaceObjList is None:
            replaceObject = None
            print("本次操作没有选择对应替换区域或替换图像,如果要执行后续操作,请重新选择。")
        else:
            replaceImg, replaceRect = replaceObjList[-1]
            if replaceRect is not None:
                targetReplaceImg = videoOperation.adjuestImgAccordingRefImg(replaceImg, logoObjList[0][-1][0])
                replaceObject = (replaceImg, replaceRect, targetReplaceImg, frame)

            else:
                print("本次操作没有选择对应替换图像,程序退出。")
                return
    print("准备工作完成,开始进行视频转换:")
    if ridLogoManner in [ridLogoManner_staticImg, ridLogoManner_frameImg]:
        ret, inf = videoOperation.convertVideo(destFName, ridLogoManner, logoObjList, replaceObject)

    elif ridLogoManner == ridLogoManner_inpaint:
        ret, inf = videoOperation.convertVideo(destFName, ridLogoManner, logoObjList, frameMask=frameMask)
    else:
        ret, inf = videoOperation.convertVideo(destFName, ridLogoManner, logoObjList, frameMask=multiFrameMask)
    print(inf)

if __name__=='__main__':
    main(ridLogoManner_multiSampleInpaint)
    

上面的代码是以最复杂的 多Logo区域采样图像修复,可以给main函数传其他参数执行其他消除方式。

4.7、注意

程序执行需注意:

  1. 如果是多Logo区域采样修复方式消除Logo,必须多次采样Logo区域图像,否则与Logo区域采样修复效果相同;
  2. 如果前三种方式Logo采样了多次,则只取最后一次采样进行处理;
  3. 视频播放采样时,通过q、Q、ESC三个键中的任意一个退出播放
  4. 视频采样时,通过n、N、s、S以及退出键都会保存当前选择的图像数据(必须画面上出现蓝色矩形);
  5. 视频采样时,鼠标左键按下会暂停播放等待采样完成,当采样完成(蓝色矩形选中且保存了当前采样区域)或放弃采样后可以通过鼠标右键点击或鼠标左键双击恢复播放;
  6. 视频采样时,蓝色边框出现后可通过重新选择范围。

五、程序执行效果

下面是一个多次Logo采样进行图像修复的运行案例截图:

1、视频Logo采样案例
采样左上角的Logo,由于“抖音”二字播放时不停晃动,需要采样多次,尽量确保“抖音”二字在不同位置都有采样,下面只提供了一次截图:
在这里插入图片描述
针对右下角的Logo信息多次截图,下面是其中的一次截图:

在这里插入图片描述

2、处理后的视频截图
在这里插入图片描述
可以看到两个角落的Logo都消除了。

六、后记

在本节基础上,老猿使用PyQt开发了一个视频Logo消除的图形化界面工具,具体开发过程请见《Python音视频:开发消除抖音短视频Logo的图形化工具过程详解》。

更多moviepy的介绍请参考《PyQt+moviepy音视频剪辑实战文章目录》或《moviepy音视频开发专栏》。这2个专栏内容的导读请参考《Python音视频剪辑库MoviePy1.0.3中文教程导览及可执行工具下载》。

关于老猿的付费专栏

老猿的付费专栏《使用PyQt开发图形界面Python应用》专门介绍基于Python的PyQt图形界面开发基础教程,付费专栏《moviepy音视频开发专栏》详细介绍moviepy音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,两个专栏加起来只需要19.9元,都适合有一定Python基础但无相关专利知识的小白读者学习。这2个收费专栏都有对应免费专栏,只是收费专栏的文章介绍更具体、内容更深入、案例更多。

付费专栏文章目录:《moviepy音视频开发专栏文章目录》、《使用PyQt开发图形界面Python应用专栏目录》。本文对应的付费专栏文章为《Python音视频开发:消除抖音短视频Logo和去电视台标的实现详解》。

关于Moviepy音视频开发的内容,请大家参考《Python音视频剪辑库MoviePy1.0.3中文教程导览及可执行工具下载》的导览式介绍。

对于缺乏Python基础的同仁,可以通过老猿的免费专栏《专栏:Python基础教程目录》从零开始学习Python。

如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

跟老猿学Python!

☞ ░ 前往老猿Python博文目录

posted on 2020-09-08 22:13  老猿Python  阅读(823)  评论(0编辑  收藏  举报