☞ ░ 前往老猿Python博文目錄 ░
一、引言
對於帶Logo(如抖音Logo、電視台標)的視頻,有三種方案進行Logo消除:
- 直接將對應區域用對應圖像替換;
- 直接將對應區域模糊化;
- 通過變換將要去除部分進行填充。
其中:
方法1又可以使用三種方法,一是使用某固定圖像替換、二是截取視頻某幀的一部分圖像替換、三是用每幀固定區域的圖像替換當前幀的Logo區域,其中固定圖像替換最簡單,下面就不展開介紹;截取視頻某幀的一部分圖像比較簡單,用每幀固定區域的圖像替換當前幀的Logo區域最復雜;
方法2可以認為是方法3的特例,即填充值來源於簡單計算,如Logo區域像素的均值等,我們在此不進行介紹。
方法3是以Logo去除后根據原Logo區域附近的圖像像素對Logo區域進行插值填充,以確保填充后的圖像整體比較協調、完整。
二、需要解決的問題
- 怎么確認Logo區域?當然是使用鼠標選擇確認Logo區域最方便;
- 使用圖像去替換Logo區域時,在鼠標選擇過程中怎么確保替換圖像大小與被替換圖像大小一致?這個需有將替換圖像進行裁剪或填充;
- 通過變換將要去除部分進行填充時,怎么確保填充值與整體視頻比較協調?本文采用根據Logo鄰近像素進行插值填充
- 對於抖音這種在晃動的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)
補充說明:
- 閾值判斷時,是以小於等於閾值和大於閾值作為分界條件
- 如果是32位彩色圖像,則是以RGB每個通道的值單獨與閾值進行比較,按每個通道進行閾值處理,返回的是一個閾值處理后的RGB各自的值
3.8、圖像膨脹處理
關於膨脹處理的知識解釋有點復雜,請參考《OpenCV-Python學習—形態學處理》以及《Opencv python 錨點anchor位置及borderValue的改變對膨脹腐蝕的影響》。
圖像的膨脹處理會使得圖像中較亮的區域增大,較暗的區域減小。
四、具體實現
本部分介紹的內容對Logo去除采用了如下四種方式:
- 使用視頻中某幀圖像的指定區域內容替換Logo
- 使用視頻中每幀圖像的指定區域內容替換當前幀的Logo區域
- Logo區域采用圖像修復
- 多Logo區域采樣圖像修復
其中第四種方法是Logo區域的Logo在視頻中為晃動的內容(如抖音的Logo)時需要,如果是靜止不變的Logo用第三種方法就夠了。
以上四種處理方式,對應的消除Logo方法類型分別為:
ridLogoManner_staticImg = 1
ridLogoManner_frameImg = 2
ridLogoManner_inpaint = 3
ridLogoManner_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、注意
程序執行需注意:
- 如果是多Logo區域采樣修復方式消除Logo,必須多次采樣Logo區域圖像,否則與Logo區域采樣修復效果相同;
- 如果前三種方式Logo采樣了多次,則只取最后一次采樣進行處理;
- 視頻播放采樣時,通過q、Q、ESC三個鍵中的任意一個退出播放
- 視頻采樣時,通過n、N、s、S以及退出鍵都會保存當前選擇的圖像數據(必須畫面上出現藍色矩形);
- 視頻采樣時,鼠標左鍵按下會暫停播放等待采樣完成,當采樣完成(藍色矩形選中且保存了當前采樣區域)或放棄采樣后可以通過鼠標右鍵點擊或鼠標左鍵雙擊恢復播放;
- 視頻采樣時,藍色邊框出現后可通過重新選擇范圍。
五、程序執行效果
下面是一個多次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。
如果有興趣也願意支持老猿的讀者,歡迎購買付費專欄。