Hi friends,
I am making a video viewer/annotator app. This app accepts dragging and dropping, tiles videos of a few seconds, and allows users to click them. The problem is that when I increase the tiling from 12 (4*3) to 15 (3x5) or 16 (4x4), the app hangs up. Until 12, the app works. According to my dirty print debugging, videos are successfully loaded using MediaCtrl.Load() . What is the cause of this problem, and what is the fix? Thanks!
Mac OS Ventura
Python 3.9.16 + miniconda
invoke the app using pythonw
App code here
import os
import wx
import wx.media
import csv
class ImagePanel(wx.Panel):
def __init__(self, parent, imagePath, app_instance):
super().__init__(parent, size=(150, 150))
self.SetBackgroundColour('white')
self.app = app_instance
self.imagePath = imagePath
self.mc = wx.media.MediaCtrl(self, style=wx.SIMPLE_BORDER, size=(140,100))
self.quote = wx.StaticText(self, label=os.path.basename(self.imagePath), pos=(0, 120))
self.Bind(wx.media.EVT_MEDIA_LOADED, self.play)
self.Bind(wx.media.EVT_MEDIA_FINISHED, self.replay)
self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
self.parent = parent
self.isSelected = False
if imagePath in self.app.selected_images:
self.SetBackgroundColour('red')
self.isSelected = True
wx.CallAfter(self.load, self.imagePath)
def load(self, path):
if self.mc.Load(path):
print('load ok')
pass
else:
print("Media not found")
self.quit(None)
def play(self, evt):
if self.mc.Play():
print('play ok')
pass
else:
print("bad playing")
def replay(self, event):
self.mc.Stop()
print(self.mc.Play())
def OnClick(self, event):
self.isSelected = not self.isSelected
if self.isSelected:
self.SetBackgroundColour('red')
self.app.selected_images.add(self.imagePath)
else:
self.SetBackgroundColour('white')
self.app.selected_images.discard(self.imagePath)
self.Refresh()
class App(wx.Frame):
def __init__(self, parent, id, title):
super().__init__(parent, id, title, size=(800, 800), style=wx.DEFAULT_FRAME_STYLE)
self.selected_images = set()
self.cols = 4
self.rows = 3 # changing from 3 to 4 causes crashes
self.InitUI()
self.current_page = 0
self.images_per_page = self.cols * self.rows
self.total_images = []
self.SetDropTarget(FileDropTarget(self))
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPress)
self.Show()
def InitUI(self):
self.control_panel = wx.Panel(self)
self.scroll_panel = wx.ScrolledWindow(self)
self.scroll_panel.SetScrollbars(1, 1, 1000, 1000)
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.images_sizer = wx.GridSizer(cols=self.cols, rows= self.rows, hgap=5, vgap=5)
self.prev_button = wx.Button(self.control_panel, label='Prev')
self.next_button = wx.Button(self.control_panel, label='Next')
self.page_text = wx.StaticText(self.control_panel, label="Page 0/0")
self.button_sizer.Add(self.prev_button, 0, wx.ALL, 5)
self.button_sizer.Add(self.next_button, 0, wx.ALL, 5)
self.button_sizer.Add(self.page_text, 0, wx.ALL, 5)
self.control_panel.SetSizer(self.button_sizer)
self.scroll_panel.SetSizer(self.images_sizer)
self.main_sizer.Add(self.control_panel, 0, wx.EXPAND | wx.ALL, 5)
self.main_sizer.Add(self.scroll_panel, 1, wx.EXPAND | wx.ALL, 5)
self.SetSizer(self.main_sizer)
self.prev_button.Bind(wx.EVT_BUTTON, self.OnPrev)
self.next_button.Bind(wx.EVT_BUTTON, self.OnNext)
self.Layout()
def UpdatePageText(self):
total_pages = (len(self.total_images) - 1) // self.images_per_page + 1
self.page_text.SetLabel(f"Page {self.current_page + 1}/{total_pages}")
def ShowImages(self, images):
self.total_images = images
self.current_page = 0
self.UpdatePageText()
self.ShowImagesPage(self.current_page)
def ShowImagesPage(self, page):
start_index = page * self.images_per_page
end_index = start_index + self.images_per_page
page_images = self.total_images[start_index:end_index]
for child in self.scroll_panel.GetChildren():
child.Destroy()
self.images_sizer.Clear(True)
for img_path in page_images:
panel = ImagePanel(self.scroll_panel, img_path, self)
self.images_sizer.Add(panel, 0, wx.ALL, 5)
self.scroll_panel.Layout()
self.images_sizer.Layout()
self.UpdatePageText()
def OnPrev(self, event):
if self.current_page > 0:
self.current_page -= 1
self.ShowImagesPage(self.current_page)
def OnNext(self, event):
if (self.current_page + 1) * self.images_per_page < len(self.total_images):
self.current_page += 1
self.ShowImagesPage(self.current_page)
def OnKeyPress(self, event):
if event.GetKeyCode() == ord('E'):
if self.total_images:
first_folder = os.path.dirname(self.total_images[0])
csv_path = os.path.join(first_folder, 'image_selection.csv')
with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['filename', 'value']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for image_path in self.total_images:
filename = os.path.basename(image_path)
value = 1 if image_path in self.selected_images else 0
writer.writerow({'filename': filename, 'value': value})
print(f"CSV file has been saved to: {csv_path}")
else:
print("No images were loaded.")
self.Close(True)
event.Skip()
class FileDropTarget(wx.FileDropTarget):
def __init__(self, window):
super().__init__()
self.window = window
def OnDropFiles(self, x, y, folders):
if len(folders) != 1 or not os.path.isdir(folders[0]):
print("Please drop a single folder.")
return False
data_path = os.path.join(folders[0], 'data')
if not os.path.exists(data_path):
self.window.Destroy()
images = []
for file in os.listdir(os.path.join(folders[0], "data")):
if file.lower().endswith(('.mp4')):
images.append(os.path.join(folders[0], "data",file))
images.sort()
self.window.ShowImages(images)
return True
app = wx.App(False)
App(None, -1, 'Viewer')
app.MainLoop()
For creating test data, I used the following code.
import os
from PIL import Image, ImageDraw, ImageFont
import av
width, height = 384, 192
duration = 4
fps = 120
total_frames = duration * fps
total_videos = 16
for i in range(total_videos):
output_filename = f'output_video_{i}.mp4'
container = av.open(output_filename, mode='w')
stream = container.add_stream('libx264', rate=fps)
stream.width = width
stream.height = height
stream.pix_fmt = 'yuv420p'
for frame_number in range(total_frames):
brightness = int(255 * (20 + (60 * frame_number / total_frames)) / 100)
image = Image.new('RGB', (width, height), (brightness, brightness, brightness))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
text = str(frame_number)
draw.text((100,100), text, fill=(255, 255, 255), font=font)
draw.text((200,100), 'video number' + str(i), fill=(255, 255, 255), font=font)
frame = av.VideoFrame.from_image(image)
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode():
container.mux(packet)
container.close()
print('Finish creating videos')