写点什么

使用 Python 实现视频 Logo 消除处理

用户头像
老猿Python
关注
发布于: 3 小时前
使用Python实现视频Logo消除处理

一、引言

对于带 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.9、图像膨胀处理

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


四、具体实现

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

  1. 使用视频中某帧图像的指定区域内容替换 Logo

  2. 使用视频中每帧图像的指定区域内容替换当前帧的 Logo 区域

  3. Logo 区域采用图像修复

  4. 多 Logo 区域采样图像修复

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

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

ridLogoManner_staticImg = 1ridLogoManner_frameImg = 2ridLogoManner_inpaint = 3ridLogoManner_multiSampleInpaint = 4
复制代码


4.1、实现思路

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

展现视频并设置鼠标回调函数;

识别鼠标动作用鼠标在视频图像中圈定 Logo 位置;

根据不同方法确认是否需要选择替换图像;

对视频中的每帧图像进行图像处理。

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 都消除了。


六、小结

本文详细介绍了消除视频 Logo 图标的几种方法以及涉及的背景知识,并提供了一套 Python+Moviepy+OpenCV 实现的消除视频 Logo 的代码。通过本文,可以了解视频剪辑处理结合 OpenCV 图像融合操作的相关知识。


用户头像

老猿Python

关注

学问无遗力,功夫老始成。 2020.08.21 加入

InfoQ签约作者,CSDN 2020年博客之星季军、高级程序员、超50万行C语言项目开发经验 擅长领域:Python语言、PyQt界面程序开发、Moviepy音视频剪辑、OpenCV-Python图像处理、爬虫、5G、区块链、人工智能数学基础

评论

发布
暂无评论
使用Python实现视频Logo消除处理