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博文目錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章