➣ Reading Time: 13 minutes

看完這篇文章你會得到的成果圖

  • 多了一條滑條,我們可以直接控制,另外我們也可以直接透過滑條來操控進度
  • 另外這次有解決上一篇 lag 的問題,會說明原因以及解法。

此篇文章的範例程式碼 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day26_video_player_add_slider_project

之前內容的重點複習 (前情提要)

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。

設計我們的 UI

主要就是新增滑條的部分,新元素的名稱:

  • self.slider_videoframe:滑條

轉換 day26.ui -> UI.py

pyuic5 -x day26.ui -o UI.py

執行看看 UI.py 畫面是否如同我們想像

一樣,這程式只有介面 (視覺上的呈現),沒有任何互動功能

  • 看看我們製作出來的介面
python UI.py

設計我們的 controller

使用 QSlider

我們已經在 【PyQt5】Day 14 – 使用 QSlider 製作可拖曳的滑條 有詳細的教學 QSlider 該如何使用了,
這邊我們就直接使用吧!

def init_video_info(self):
    self.ui.slider_videoframe.setRange(0, self.video_total_frame_count-1)
    self.ui.slider_videoframe.valueChanged.connect(self.getslidervalue)

def __get_frame_from_frame_no(self, frame_no):
    self.setslidervalue(frame_no)

def getslidervalue(self):
    self.current_frame_no = self.ui.slider_videoframe.value()

def setslidervalue(self, value):
    self.ui.slider_videoframe.setValue(self.current_frame_no)

1. 取得滑條值的部分

我們在 init_video_info() 新增了關於滑條初始化的功能,
我們設定好這個滑條的 range 為 (0, 全部 frame 數 -1),
並且將這個滑條連結於 getslidervalue() 的功能上,
只要我們移動滑條,就會啟動這個函數。

2.變更滑條值的部分

我們製作了一個函數 setslidervalue(),當我們更改 frame 的時候,
我們可以直接也更改滑條的值。

而呼叫這個 setslidervalue() 的 function 位於取得 frame from frame number 的時候,
也同步呼叫這個函數,就可以完成「隨著 frame 變化更改滑條的值」。

優化我們的播放器效能 (解決昨天的 lag 問題)

昨天我們提到我們程式執行的時候會有 lag 的問題,
那時我是直接給個優化的方向,是我們可以考慮提程式的 decode 加入「multiprocessing」的平行運算功能。
不過今天處理的過程中,我稍微替我的程式加了幾個計時器,
後來意外發現卡住的 function 其實只有一個,這樣就好處理了!

處理我們先前程式的 bottleneck

以這支程式來說,很直覺的我會認為會慢都是牽扯到 decode 那一段的速度,
因為處理圖片基本上就是最花時間的地方…

因此我加了一些 timer 在昨天的 code

def __get_frame_from_frame_no(self, frame_no):
    time_start = time.time()
    self.vc.set(1, frame_no)
    ret, frame = self.vc.read()
    time_end = time.time()
    print(time_end - time_start)

我們來計時一下,這段處理到底花了多少時間。
結果發現了一個很有趣的現象:

  • stop 時,平均一個 frame 只需要處理 0.01~0.02 秒左右
  • 一但進入 start 或 pause 的狀態,平均一個 frame 需要處理 0.06~0.07 秒左右

這就很奇怪了!!! 照理來說處理一個圖片,應該也不會到有那麼大的誤差。
而且是平均時間,還不是幾張圖片或許資訊比較豐富所以處理比較久。

這表示我們設計的機制一定有什麼可優化的問題。

再繼續往下查,抓出產生問題的關鍵 function

最後我們發現一件有趣的事情:

原本我以為是處理處片的時間很久,結果只花了 0.001 秒

ret, frame = self.vc.read()

然而卻是以下這行,設定人在哪個 frame 的函數,可能會造成約 0.05 秒左右的延遲。

self.vc.set(1, frame_no)

但是,我之前做過的專案經驗告訴我,正常來說的解碼不會那麼久,
所以一定是我不夠正確的使用這一行。

所以我決定修改機制。

重新設計使用 vc.set() 的機制,減少使用

照官方文件的定義,即使沒有 vc.set(),只需要一直 vc.read() 也能夠一直往下取 frame,

我猜可能這就是原因了,因為 OpenCV (或說是他使用的 ffmpeg library) 在 decode 的時候,
針對連續的 frame 有做優化的演化法,
所以如果我每次都重新設定第幾個 frame,會導致這個優化演算法失效
可以想像是,因為我們的影片都是連續的,
所以搞不好可以透過計算向量差的方式,更快的算出下一張圖片。(而這機制被我的設計弄到失效)

於是我們更改一下原本的邏輯,「只要必須要設定 frame 時,才使用 vc.set()」

把顯示 frame 的函數拆成兩個 function

我們把 self.vc.set(1, frame_no) 這個會造成 bottleneck 的 funciton 獨立出來。

def set_current_frame_no(self, frame_no):
    self.vc.set(1, frame_no) # bottleneck

def __get_next_frame(self):
    ret, frame = self.vc.read()
    self.ui.label_framecnt.setText(f"frame number: {self.current_frame_no}/{self.video_total_frame_count}")
    self.setslidervalue(self.current_frame_no)
    return frame

配合上述機制的修改對應設計

def timer_timeout_job(self):
    if (self.videoplayer_state == "play"):
        if self.current_frame_no >= self.video_total_frame_count-1:
            #self.videoplayer_state = "pause"
            self.current_frame_no = 0 # auto replay
            self.set_current_frame_no(self.current_frame_no)
        else:
            self.current_frame_no += 1

    if (self.videoplayer_state == "stop"):
        self.current_frame_no = 0
        self.set_current_frame_no(self.current_frame_no)

    if (self.videoplayer_state == "pause"):
        self.current_frame_no = self.current_frame_no
        self.set_current_frame_no(self.current_frame_no)

    frame = self.__get_next_frame() 
    self.__update_label_frame(frame)

原本會更新畫面的函數位置不變,而我們在 pause、stop、與影片播放完畢後,
都啟用 set_current_frame_no() 這個函數,才會去啟動 vc.set() 修改 frame index。

def getslidervalue(self):
    self.current_frame_no = self.ui.slider_videoframe.value()
    self.set_current_frame_no(self.current_frame_no)

另外一個也會影響到 frame index 的就是滑條,
我們也是在滑條「被移動」的時候,才會去呼叫 set_current_frame_no() 啟動 vc.set()

測試結果

我的影片播放器終於順暢了!!! 耶!!!

此外昨天保留的計算 fps 機制就可以拿回來用了。
如果不想要讓影片已超快的 1ms 更新,可以改回上面 timer 的做法。

self.timer.start(1000//self.video_fps) # start Timer, here we set '1000ms//Nfps' while timeout one time
self.timer.start(1) # but if CPU can not decode as fast as fps, we set 1 (need decode time)

Reference

⭐Python PyQt5 相關文章整理⭐:
⭐基礎知識與架構篇⭐:
1.【PyQt5】Day 1 – 安裝 PyQt,建立自己的第一支 PyQt5 程式
2.【PyQt5】Day 2 – 利用 Qt designer 建立第一支有自己介面的 PyQt5 程式
3.【PyQt5】Day 4 – 重要的 Qt 程式邏輯觀念,務必先有此觀念後面才會懂自己在幹嘛
4.【PyQt5】Day 5 – 開始來設計我們的 controller.py,改以「程式角度」來說明如何建立 PyQt 的系統
⭐基本元件應用篇⭐:
1.【PyQt5】Day 6 – 我們的第一個 output 手段 – Qlabel
2.【PyQt5】Day 7 – 我們的第一個 input 手段 – QPushButton
3.【PyQt5】Day 8 – 我們的第二個 input 手段 – QLineEdit
4.【PyQt5】Day 9 – 以 QLineEdit, QTextEdit, QPlainTextEdit 作為文字的輸入
6.【PyQt5】Day 14 - 使用 QSlider 製作可拖曳的滑條
8.【PyQt5】Day 19 - 使用 QProgressBar,製作進度條的功能
⭐介面系統控制篇⭐:
5.【PyQt5】Day 10 – 以 QFileDialog 讀取系統的檔案、資料夾
7.【PyQt5】Day 18 / Project 使用 QTimer,自製碼表(計時器) PyQt5 stopwatch DIY
1.【PyQt5】Day 20 - PyQt 最重要的 QThread 概念 / 為什麼 windows, mac, ubuntu (linux) 程式會「沒有回應」?
2.【PyQt5】Day 21 – 透過 PyQt 實現滑鼠監聽總整理,完全掌握滑鼠控制 (listen mouse)
3.【PyQt5】Day 22 – PyQt 視窗的個性化/屬性控制 setWindowFlags,禁止放大縮小、永遠顯示於最上層/最下層
4.【PyQt5】Day 23 – 使用系統內建的調色盤 QColorDialog,來替我們選擇顏色 QColor (Color Picker)
5.【PyQt5】Day 24 / Project 偵測滑鼠目前指示顏色的小工具 (滴管工具), 利用 QCursor 偵測滑鼠, QApplication 取得截圖
⭐影像處理篇⭐:
1.【PyQt5】Day 11 – 以 Qlabel 在 PyQt 中顯示圖片 (基於 QImage 使用 OpenCV)
2.【PyQt5】Day 12 – 建立一個可以縮放圖片大小的顯示器 (基於 QImage 使用 OpenCV)
3.【PyQt5】Day 13 – 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV)
4.【PyQt5】Day 15 / Project 與檔案功能整合,製作出可讀取圖片並可縮放的 UI 介面 (使用 PyQt + OpenCV)
5.【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理
6.【PyQt5】Day 17 / Project 製作標註 roi 工具, 開始導入 OpenCV 作為繪圖引擎, 在圖上畫點並顯示座標
⭐project 篇⭐:
1.【PyQt5】Day 25 / Project 自己做一個影片播放器 DIY video player (結合 PyQt + OpenCV)
2.【PyQt5】Day 26 / Project 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)
3.【PyQt5】Day 27 / Project 製作影片 ROI 標註工具 (PyQt 結合 OpenCV 在圖上畫點畫線)
4.【PyQt5】Day 28 / final project – 1 來搞一個自己的 photoshop 吧!UI 篇 + 純程式架構篇 (結合 PyQt + OpenCV)
5.【PyQt5】Day 29 / final project – 2 來搞一個自己的 photoshop 吧!後段程式細節篇 (結合 PyQt + OpenCV)
6.【PyQt5】Day 30 / final project – 3 來搞一個自己的 photoshop 吧!把每個方法封裝起來製作出還原功能吧! (結合 PyQt + OpenCV)
⭐打包程式篇⭐:
1.【PyQt5】Day 3 – 用 pyinstaller 將 python 程式打包,把每天的成果分享給你的親朋好友
⭐【喜歡我的文章嗎? 歡迎幫我按讚~ 讓基金會請創作者喝一杯咖啡!
如果喜歡我的文章,請幫我在下方【按五下Like】 (Google, Facebook 免註冊),會由 「LikeCoin」 贊助作者鼓勵繼續創作,讀者們「只需幫忙按讚,完全不用出錢」哦!

likecoin-steps