質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.91%

wx.StaticBitmap.SetBitmap()でbitmapをセットすると空白画像が表示される

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 736

amikappa

score 6

StaticBitmap.SetBitmap()でbitmapを表示したい

以下の環境にて、wxPythonで動画を表示させるStaticBitmapの拡張クラスを作ろうとしています。
・Python 3.6.8
・wxPython 4.0.6
・OpenCV 4.1.1
・ubuntu 18.06

OpenCVはapp.MainLoop()でループさせる必要があるため、StaticBitmapを継承したクラスを作って、その中で別スレッドを作り、その中でループさせてフレームを読み込み、bitmapに変換して自身に再セットする、といった処理を考えています。

おおむね上手くいったのですが、なぜかbitmapの再セットをSetBitmap()で行うと、画像が表示されず、真っ白になります。

スレッド操作が悪いのか、StaticBitmapの使い方が悪いのか、原因の切り分けができずに困っています。

どなたか、wxPython+OpenCVでStaticBitmapを使って動画再生をしたことがあるかたがいらっしゃったら、アドバイスを頂けると助かります。

該当のソースコード

import wx
import cv2
import numpy as np
import threading

class VideoBitmap(wx.StaticBitmap):
    def __init__(self, parent, file_name, pos=(0, 0), size=(100, 100)):
        self.video = cv2.VideoCapture(file_name)
        self.width = size[0]
        ret, frame = self.video.read()
        bmp = self.create_wx_bitmap_from_cv2_image(frame)
        bmp = self.scale_bitmap_keep_ratio(bmp, 300)
        super().__init__(parent, wx.ID_ANY, bmp, pos, size, style=0, name='')

    def create_wx_bitmap_from_cv2_image(self, cv2_image):
        height, width = cv2_image.shape[:2]
        cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
        return wx.BitmapFromBuffer(width, height, cv2_image_rgb)

    def scale_bitmap_keep_ratio(self, bitmap, width):
        image = wx.ImageFromBitmap(bitmap)
        rate = bitmap.Height / bitmap.Width
        height = width * rate
        image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH)
        result = wx.BitmapFromImage(image)
        return result

    def video_run(self):
        frame_count = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT))
        frame_rate  = int(self.video.get(cv2.CAP_PROP_FPS))

        test_bmp = wx.Bitmap('icons/save.png')
        for i in range(frame_count):
            is_read, frame = self.video.read()
            if not is_read:
                break
            bmp = self.create_wx_bitmap_from_cv2_image(frame)
            bmp = self.scale_bitmap_keep_ratio(bmp, 300)
            self.SetBitmap(bmp)
            print('%d' % i)

if __name__ == "__main__":

    app = wx.App(False)

    file_name = "video/video.mp4"

    frame = wx.Frame(None, wx.ID_ANY, "テスト", size=(800,625))
    panel = wx.Panel(frame, size=(800,625))
    layout = wx.FlexGridSizer(rows=2, cols=2, gap=(0,0))
    panel.SetSizer(layout)

    video = VideoBitmap(panel, file_name, pos=(10, 10), size=(300, 300))
    layout.Add(video)
    frame.Show(True)

    thread = threading.Thread(target=video.video_run)
    thread.start()

    app.MainLoop()

試したこと

上記コードのメソッド「video_run」内にある「self.SetBitmap(bmp)」がうまく機能していないと思われるため、以下を試しました。
・初期表示の画像(コンストラクタで作成したbmpをself.init_bmpとして保持)を、bmpの代わりにself.SetBitmap(self.init_bmp)として表示してみたが、やはり真っ白になった。
・適当なアイコン画像をwx.Bitmap()で生成してSetBitmapに入れてみたところ、最初の1フレームループの時だけ表示され、2巡目からはやはり真っ白になる。
・self.Update()で即時描画更新を試したが、コアダンプで落ちる。

コンストラクタでセットした画像だけ(つまり初手)は表示されます。

アドバイスを頂けると幸いです。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

wxのGUI制御がどこまでマルチスレッドに対応しているか不明なので画面更新をスレッドで行うのはやめたほうがいいのではと思います。

タイマー処理を使うのが現実的かもしれません。

import wx
import cv2
import numpy as np
import threading

class VideoBitmap(wx.StaticBitmap):
    def __init__(self, parent, file_name, pos=(0, 0), size=(100, 100)):
        self.video = cv2.VideoCapture(file_name)
        self.width = size[0]
        ret, frame = self.video.read()

        self.frame_bmps = []
        self.next_frame_bmp_no = 0;
        while True:
            is_read, frame = self.video.read()
            if not is_read:
                break
            bmp = self.create_wx_bitmap_from_cv2_image(frame)
            bmp = self.scale_bitmap_keep_ratio(bmp, 300)
            self.frame_bmps.append(bmp)

        super().__init__(parent, wx.ID_ANY, self.frame_bmps[self.next_frame_bmp_no], pos, size, style=0, name='')
        self.next_frame_bmp_no += 1

        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnTimer)
        self.timer.Start(1000/int(self.video.get(cv2.CAP_PROP_FPS)))

    def create_wx_bitmap_from_cv2_image(self, cv2_image):
        height, width = cv2_image.shape[:2]
        cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
        return wx.BitmapFromBuffer(width, height, cv2_image_rgb)

    def scale_bitmap_keep_ratio(self, bitmap, width):
        image = wx.ImageFromBitmap(bitmap)
        rate = bitmap.Height / bitmap.Width
        height = width * rate
        image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH)
        result = wx.BitmapFromImage(image)
        return result

    def OnTimer(self, event):
        self.SetBitmap(self.frame_bmps[self.next_frame_bmp_no])
        self.next_frame_bmp_no += 1
        self.next_frame_bmp_no %= len(self.frame_bmps)

if __name__ == "__main__":

    app = wx.App(False)

    file_name = "video/video.mp4"

    frame = wx.Frame(None, wx.ID_ANY, "テスト", size=(800,625))
    panel = wx.Panel(frame, size=(800,625))
    layout = wx.FlexGridSizer(rows=2, cols=2, gap=(0,0))
    panel.SetSizer(layout)

    video = VideoBitmap(panel, file_name, pos=(10, 10), size=(300, 300))
    layout.Add(video)
    frame.Show(True)

    #thread = threading.Thread(target=video.video_run)
    #thread.start()

    app.MainLoop()

スレッドを使わなければならない場合(メモリ節約やMainスレッドをできるだけMainLoopにいさせたい場合)はwx.PostEventを使用する方法もあると思います。
スレッド側でbmp生成まで行った後、そのデータをイベントの付加データとしてメインスレッドに投げて、メインスレッドで描画更新をするという流れになります。(その場合、フレーム間隔をどうするかは別途考える必要あり)

参考記事:マルチスレッドでwxPython

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/09/24 12:29

    スレ主です。さっそく丁寧なレスを頂き、ありがとうございました。
    今、会社にいるので試せませんが、帰宅したらさっそく試してみようと思います。
    wxPythonがマルチスレッドに対応していないかもしれないとは思わなかったので、目からウロコでした。ありがとうございます。
    試したら、また返信しますね。

    キャンセル

  • 2019/09/24 21:57

    スレ主です。試したところ、ご指南いただいたコードでうまくいきました。
    やはり、wxPythonはマルチスレッドだと正常に動作しない(画像表示の更新ができない?)ようです。

    しかし、コンストラクタで全フレームをBMPにすると、メモリの消費量がすさまじいことになったので、PostEventでの実装も検討してみようと思います。
    非常に貴重なご意見を頂き、ありがとうございました。
    突破口が見えたので、ひとまず安心できました。

    キャンセル

  • 2019/09/25 20:25

    スレ主です。
    ご指南いただいたコードを少々改造しました。
    ・コンストラクタで動画全部をBMP変換してバッファリングするのでなく、Timerイベント内で1枚づつ読み込んでBMP変換する

    この方法だと、私のオンボロパソコン(Atomノートにlubuntuを入れて無理やり使っている)だと変換処理が間に合わず、絵が出ないことがありましたので、Timerイベントの最初でTimerをOFFにし、変換が修理したらまたONにするよう工夫しています。
    これでフレームレートが落ちますが、ちゃんと再生できました。パソコンが普通に速ければフレームレート落ちもしないと思います。
    ソースを貼ります。
    どなたか、同様のお悩みの方の参考になれば幸いです。
    ***
    import wx
    import cv2
    import numpy as np

    class VideoBitmap(wx.StaticBitmap):
    def __init__(self, parent, file_name, pos=(0, 0), size=(100, 100)):
    self.video = cv2.VideoCapture(file_name)
    self.width = size[0]
    ret, frame = self.video.read()

    is_read, frame = self.video.read()
    bmp = self.create_wx_bitmap_from_cv2_image(frame)
    bmp = self.scale_bitmap_keep_ratio(bmp, 300)

    super().__init__(parent, wx.ID_ANY, bmp, pos, size, style=0, name='')

    self.count = 0

    self.timer = wx.Timer(self)
    self.Bind(wx.EVT_TIMER, self.OnTimer)
    self.interval = 1000/int(self.video.get(cv2.CAP_PROP_FPS))
    self.timer.Start(self.interval)

    def create_wx_bitmap_from_cv2_image(self, cv2_image):
    height, width = cv2_image.shape[:2]
    cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
    return wx.Bitmap.FromBuffer(width, height, cv2_image_rgb)

    def scale_bitmap_keep_ratio(self, bitmap, width):
    image = bitmap.ConvertToImage()
    rate = bitmap.Height / bitmap.Width
    height = width * rate
    image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH)
    result = wx.Bitmap(image)
    return result

    def OnTimer(self, event):
    self.timer.Stop()
    is_read, frame = self.video.read()
    if not is_read:
    return
    bmp = self.create_wx_bitmap_from_cv2_image(frame)
    bmp = self.scale_bitmap_keep_ratio(bmp, 300)
    self.SetBitmap(bmp)
    self.count += 1
    print('%d' % self.count)
    self.timer.Start(self.interval)

    if __name__ == "__main__":

    app = wx.App(False)

    file_name = "video/video.mp4"

    frame = wx.Frame(None, wx.ID_ANY, "テスト", size=(800,625))
    panel = wx.Panel(frame, size=(800,625))
    layout = wx.FlexGridSizer(rows=2, cols=2, gap=(0,0))
    panel.SetSizer(layout)

    video = VideoBitmap(panel, file_name, pos=(10, 10), size=(300, 300))
    layout.Add(video)
    frame.Show(True)

    app.MainLoop()
    ***

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.91%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る