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

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

ただいまの
回答率

89.07%

wxpythonで透過gifと背景gifを同時に重ねて再生表示する方法

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 76

dendenmushi

score 82

前提・実現したいこと

wxpythonでgif動画を再生するデスクトップアプリを作っています。
gif動画の一方を透過色が入ったもので、2重に再生し、ボタンも表示したい。

理想は以下のイメージになります。
イメージ説明

ソースコード

同フォルダにあるgif画像を10秒おきに入れ替え再生していますが、同時に再生したいと思っております。

import os
import sys
import time
from threading import Thread

import wx
from wx.adv import Animation, AnimationCtrl
from wx.lib.newevent import NewEvent

UpdateImageEvent, EVT_UPDATE_IMAGE = NewEvent()


class CountTimer(Thread):
    daemon = True

    def __init__(self, win):
        super().__init__()

        # イベント通知対象のウィジェット。(MainWindow)
        self.win = win

        # 次の画像ファイルを所得する関数 (仮: カレントディレクトリの gif ファイルが対象)
        from glob import glob
        from itertools import cycle
        from functools import partial
        self.nextImageFile = partial(next, cycle(glob("*.gif")))

    def run(self):
        num = 0
        while True:
            num += 1
            if num % 10 == 0: # 剰余で十秒毎を判別

                # 次に表示する画像ファイル
                filepath = self.nextImageFile()

                # イベント通知する為、引数となるイベントオブジェクトを作成
                event = UpdateImageEvent(filepath=filepath)

                # 通知 (MainLoopのキューにイベントを入れる)
                wx.PostEvent(self.win, event)

                del event, filepath
            time.sleep(1)


class MainWindow(wx.Frame):
    def __init__(self, parent=None, ID=wx.ID_ANY, title="", *args, **kw):
        super().__init__(parent, ID, title, *args, **kw)
        self.animeCtrl = AnimationCtrl(self)

        # イベントとイベント通知受信時に呼び出されるコールバック関数の結び付け
        self.Bind(EVT_UPDATE_IMAGE, self.OnUpdateImage)

    def OnUpdateImage(self, event):
        # PostEvent での通知により *メインスレッドで* 実行される
        print("OnUpdateImage", event.filepath)
        self.SetImageFile(event.filepath)

    def SetImageFile(self, filepath):
        # 画像・動画を変更し再生する
        assert os.path.isfile(filepath)
        self.animeCtrl.SetAnimation(Animation(filepath))
        self.animeCtrl.Play()


def main():
    app = wx.App()
    win = MainWindow()
    win.SetSize(400, 400)
    win.Centre()
    win.Show()

    thread = CountTimer(win)
    thread.start()

    app.MainLoop()


if __name__ == '__main__':
    main()

Architectureと発生している問題・エラーメッセージ

wx.Posteventの方法で試した場合、ひとつのeventのみ呼び出されてしまい、レイヤーのように別々に操作することができませんでした。

使用ファイル

以下に、gif動画ひとつと透過pngをgif化したものを配置しました。(透過gifになっていなかったらすみません。)
Google Drive

参考サイト

gif画像を一枚ずつの画像へ分解するサイト
透過画像作成サイト
透過pngをgif動画にするサイト
アニメーションについて調べたサイト
アニメーションについて調べたサイト2
過去teratailでの似ている質問(透過gif)

2つのgifを重ねて一枚一枚保存したものをgifにしてそれを1つのイベントで実行という方法がありますが、
私は、あるイベント発生(クリックなど)によって一枚だけのレイヤーに存在する透過gifを他のものに変えたり最終的にはしたいため、イベント自体を2個同時に実行するのがよいと考えております。そもそもイベントを重ねてgifを表示することじたいが調べているのですがでてこなく、知見ある方何かアドバイス頂けないでしょうか。
よろしくお願い致します。

補足情報(FW/ツールのバージョンなど)

win10
python3.7
wxpython

追記 2020/08/02 

以下など試してみました。
《別方法1》

class MainWindow(wx.Frame):
    def __init__(self, parent=None, ID=wx.ID_ANY, title="", *args, **kw):
        super().__init__(parent, ID, title, *args, **kw)
        self.animeCtrl = AnimationCtrl(self)

        # 20200801 add
        panel1 = wx.Panel(self, wx.ID_ANY)
        v_layout = wx.BoxSizer(wx.VERTICAL)
        # ButtonA
        feature_button = wx.Button(panel1, wx.ID_ANY, "Feature")
        feature_button.Bind(wx.EVT_BUTTON, self.feature_method)
        v_layout.Add(feature_button, proportion=0, flag=wx.ALIGN_CENTER_HORIZONTAL)
        panel1.SetSizer(v_layout)

    def feature_method(self, event):
        wx.MessageBox('正解です!')


《別方法2》

def main():
    app = wx.App()
    win = MainWindow()
    win.SetSize(800, 800)
    win.Centre()
    win.Show()
    # 20200801 add
    win_sub = SubWindow()
    win_sub.SetSize(800, 800)
    win_sub.Centre()
    win_sub.Show()


SubWindowクラスを作るなど試行錯誤しましたが同時に表示はできませんでした。

《別方法3》

import wx
import wx.adv

class BG(object):#背景の表示
    def __init__(self, MainPanel,x,y):
        BGimg =wx.Image("1.png")
        self.bitmap = BGimg.ConvertToBitmap()
        wx.StaticBitmap(MainPanel, -1, self.bitmap, (x,y))

class MyWindow(wx.Frame):
    def __init__(self, parent=None, id=-1, title=None):
        wx.Frame.__init__(self, parent, id, title)
        self.MainPanel = wx.Panel(self, size=(1200, 800))#メイン画面の大きさ
        self.MainPanel.SetBackgroundColour("Black")
        self.BackG = BG(self.MainPanel, 0, 0)
        self.anime = wx.Panel(self.MainPanel, pos=(350, 0), size=(450, 800),
                              style=wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN)
        self.file = "sakura.gif"
        self.ani = wx.adv.Animation(self.file)
        self.ctrl = wx.adv.AnimationCtrl(self.anime, -1, self.ani, pos=(10, 10), style=wx.TRANSPARENT)
        self.ctrl.Play()
        self.Fit()

if __name__ == "__main__":
    app = wx.App()
    w = MyWindow(title="test")
    w.Center
    w.Show()
    app.MainLoop()


イメージ説明

この方法でも重ねることはできませんでした。

《別方法4》

import wx
import wx.adv

GIFNames = [
    'sakura.gif'
    ]


class TestPanel(wx.Panel):
    def __init__(self, parent):

        wx.Panel.__init__(self, parent, -1)

        sizer = wx.FlexGridSizer(2,3,5,5)

        for name in GIFNames:
            ani = wx.adv.Animation(name)
            ctrl = wx.adv.AnimationCtrl(self, -1, ani)
            ctrl.SetBackgroundColour('black')
            ctrl.Play()

            #sizer.AddF(ctrl, wx.SizerFlags().Border(wx.ALL, 10))

        border = wx.BoxSizer()
        #border.AddF(sizer, wx.SizerFlags(1).Expand().Border(wx.ALL, 20))
        self.SetSizer(border)

app = wx.App()
frame = wx.Frame(None)
panel = TestPanel(frame)
frame.Show()
app.MainLoop()


イメージ説明
イメージ説明

透過はできるようにはなったようなのですが、その背景に別gifを入れること(まずはバックグラウンドを黒)に挑戦していますが、黒背景もまだでませんしもう少しのようです。
そもそも、黒背景ではなく、もう一つの背景gifじたいもユーザーのアクションによって変えたいので端的に背景に挿入しても変更できないのであれば問題ですが…

《別方法5》

import wx 

class Mywin(wx.Frame): 

   def __init__(self, parent, title): 
      super(Mywin, self).__init__(parent, title = title,size = (500,300))  
      self.InitUI() 

   def InitUI(self): 
      self.Bind(wx.EVT_PAINT, self.OnPaint) 
      self.Centre() 
      self.Show(True)

   def OnPaint(self, e): 
      dc = wx.PaintDC(self) 
      brush = wx.Brush("white")  
      dc.SetBackground(brush)  
      dc.Clear() 

      dc.DrawBitmap(wx.Bitmap("1.png"),10,10,True) 
      color = wx.Colour(255,0,0)
      b = wx.Brush(color) 

      dc.DrawBitmap(wx.Bitmap("vqkSsK_Z-0.png"),10,10,True) 
      color = wx.Colour(255,0,0)
      b = wx.Brush(color)

      dc.SetBrush(b) 
      dc.DrawCircle(300,125,50) 
      dc.SetBrush(wx.Brush(wx.Colour(255,255,255))) 
      dc.DrawCircle(300,125,30) 

      font = wx.Font(18, wx.ROMAN, wx.ITALIC, wx.NORMAL) 
      dc.SetFont(font) 
      dc.DrawText("Hello wxPython",200,10) 

      pen = wx.Pen(wx.Colour(0,0,255)) 
      dc.SetPen(pen) 
      dc.DrawLine(200,50,350,50) 
      dc.SetBrush(wx.Brush(wx.Colour(0,255,0), wx.CROSS_HATCH)) 
      dc.DrawRectangle(380, 15, 90, 60) 

ex = wx.App() 
Mywin(None,'Drawing demo') 
ex.MainLoop()


イメージ説明

DrawBitmapというものを使えば重ねて表示ができそうです。ただこの場合ですとgifで対応できないようです。
近づいてはいるとは思いますが、引き続きアドバイスよろしくお願い致します。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

+1

イメージ説明

課題が幾つかありますが、重ね合わせはこんな感じでしょうか。

問題の原因、
AimationCtrl 自身が背景色を持ち、
画像が透過になっても表示されるのは、AnimationCtrl の背景色になるようです。

解決策: 独自に描画する

SubWindow を使う方法でもできそうですが、(shaped windowで検索、少し難しいです)
ウィンドウを動かした時等、他の課題も出てきそうなので、
タイマーを使って自身でアニメーション描画を実装する方法を紹介します。

※ 但し、問題が複雑化するため、画像ファイルの切り替えは、省きました。
以下のコードは透過動画の重ね合わせ部分のみです。

import wx
import wx.adv


def callLaterTimedGen(gen, done=None, stop=None):
    def next_gen():
        interval = next(gen, stop)
        if interval is not stop:
            wx.CallLater(interval, next_gen)
    wx.CallAfter(next_gen)
    return gen


class MyFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kw):
        super().__init__(parent, *args, **kw)
        self.bmp1 = None
        self.bmp2 = None
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_TIMER, lambda _: self.Refresh())

        self.timer = wx.Timer(self)
        self.timer.Start(200)

    def SetBitmap1(self, bmp):
        self.bmp1 = bmp

    def SetBitmap2(self, bmp):
        self.bmp2 = bmp

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        if self.bmp1:
            # 背景 (mask: False)
            dc.DrawBitmap(self.bmp1, 0, 0, False)
        if self.bmp2:
            # 透過 (mask: True)
            dc.DrawBitmap(self.bmp2, 0, 0, True)


def gifAnimation(filepath, setBitmap, useMask=None):
    anim = wx.adv.Animation(filepath)

    from itertools import cycle
    for idx in cycle(range(anim.GetFrameCount())):
        delay = anim.GetDelay(idx)
        frame = anim.GetFrame(idx)
        if useMask:
            frame.SetMaskColour(*useMask)
        yield delay
        setBitmap(frame.ConvertToBitmap())


def main():
    app = wx.App()
    win = MyFrame(None, wx.ID_ANY, "透過GIF重ね合わせテスト")
    win.Centre()
    win.Show()

    button1 = wx.Button(win, label="TEXT", pos=(20, 100))
    button2 = wx.Button(win, label="TEXT", pos=(20, 150))

    gen1 = callLaterTimedGen(gifAnimation("someiyoshino1.gif", win.SetBitmap1))
    gen2 = callLaterTimedGen(gifAnimation("index.gif", win.SetBitmap2, useMask=(0, 0, 0)))

    wx.CallLater(3000, gen2.close) # 3秒後に index.gif の再生を停止

    app.MainLoop()


if __name__ == '__main__':
    main()

要求知識: Pythonのジェネレータについて

  • callLaterTimedGen 関数 (ジェネレータをタイマーで読み出す)
  • gifAnimation 関数 (アニメーション処理をジェネレータとして実装)

wx.adv.Animation で提供されてるメソッド

  • GetFrameCount ... 総フレーム数
  • GetFrame ... 静止画フレーム
  • GetDelay ... アニメーションに用いる遅延時間 (ms)

アニメーションの実装はシンプルに
delay秒待ってから フレームを変換し setBitmap という処理を行ってます。

callLaterTimerGen() 関数は、
wxのタイマーでジェネレーターから値を一つづつ読み出します。
yield で返された値を次の読み出しまでの timeout 時間として用いるので、
yield delay を この関数で実行する タイマー上での sleep として扱えます。

wx については、説明は省略します。概要のみ。

  • wxPython のタイマー wx.CallLater, wx.CallAfter 指定時間後に一度のみ実行
  • wxPytnon のタイマーイベント wx.Timer 一定時間毎に定期実行
  • EVT_PAINT 描画イベントとPaintDC を使った独自描画の方法

課題:

  • 画像の切り替えですが、wx.Timer を使ったほうが簡単で良いかもしれません
    スレッド間通信の必要がなくなります。
  • index.gif は透過になっていませんでした。
  • 黒色をマスク指定しましたが、花びらの周りに淡い紫色?がありました。
  • 事前加工が可能なら、numpy, OpenCV, PIL 等で加工してください。
    参考: 画像のしきい値処理 threshold 辺りを検索
  • 画像ファイルの切り替えは幾つか方法がありますが、1案
  1. 10秒毎にタイマー関数呼び出し
  2. 前のアニメーションを止めて (gen.closeで停止) 
  3. 新アニメーションのタイマーを開始

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2020/08/02 21:25

    非常に勉強になりました。ありがとうございました。ボタンクリックで背景gifを変えようと欲がでてしまいましたが、工夫が必要なのですね。
    '''
    class MyFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kw):
    super().__init__(parent, *args, **kw)
    self.bmp1 = None
    self.bmp2 = None
    self.Bind(wx.EVT_PAINT, self.OnPaint)
    self.Bind(wx.EVT_TIMER, lambda _: self.Refresh())

    self.timer = wx.Timer(self)
    self.timer.Start(200)

    # panel = wx.Panel(self)

    #ボタンの作成
    self.Button1=wx.Button(self, label="exit",pos=(20,100))
    self.Button2=wx.Button(self, label="print",pos=(20,150))

    #ボタンを割り当て
    self.Bind(wx.EVT_BUTTON, self.close, self.Button1)
    self.Bind(wx.EVT_BUTTON, self.printer, self.Button2)

    self.Show(True)

    def close(self,event):
    self.Close(True)

    def printer(self, event):
    print("Button2")
    callLaterTimedGen(gifAnimation("someiyoshino1.gif", self.SetBitmap1)).close
    callLaterTimedGen(gifAnimation("someiyoshino2.gif", self.SetBitmap1))
    '''
    このコードにして追加で挑戦してみましたがチカチカと完全に元のが消えないですね。
    質問の件につきまして本当に助かりました。ありがとうございました。

    キャンセル

  • 2020/08/02 22:02

    最後の2行が self.SetBitmap1 になってますね。同じ画像が交互に切り替わる。
    close は 呼び出しのカッコがついてないので、このコードでは無意味になってます。

    画像切り替えは、ジェネレータオブジェクトを
    クラスのインスタンス変数等に格納しておいて、ボタンのイベントで
    self.gen1.close()
    self.gen1 = callLaterTimesGen( ... )

    キャンセル

  • 2020/08/02 22:24

    全て望みの形ができました。本当にありがとうございました。

    キャンセル

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

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

関連した質問

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