I previously used cv2 for scaling, then discovered that if you do it in hardware via a GraphicsContext it’s so fast it’s virtually free! However, there’s a strange problem:
It’s fine scaling up, or scaling down if the scale is more than 75%. As soon as it dips below that it suddenly takes 20x as long.
I’m using:
Python: 3.8.10 (default, Nov 22 2023, 10:22:35)
WXPython: 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5
WX renderer: wxCairoRenderer (1, 16, 0)
I’ve tried this on two machines. One is on Ubuntu 18, the other on Ubuntu 20. Both are using the iGPU on Intel CPUs, one is a modest N4200, the other a recent i5.
I’m not a wxPython expert so maybe I’ve done something daft, or maybe there’s a bug?
I made a minimal complete script in case you want to replicate the problem, though it’s still quite big, sorry about that:
import sys
import wx
import time
import cv2
import numpy as np
from collections import deque
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self,
None,
id=wx.ID_ANY,
title="Image Scaling Test",
pos=wx.DefaultPosition,
size=wx.DefaultSize
)
self.bgColour = wx.Colour([64, 64, 64])
self.background_brush = wx.Brush(self.bgColour, wx.SOLID)
self.initUI()
self.initVideo()
self.ShowFullScreen(True)
self.firstRun = True
self.timer1 = wx.Timer(self, id=1)
self.Bind(wx.EVT_TIMER, self.on_timer1, id=1)
self.timer1.Start(10)
def initUI(self):
self.panel_controls = wx.Panel(self)
self.panel_controls.SetBackgroundColour(self.bgColour)
controlsSizer = wx.BoxSizer(wx.HORIZONTAL)
self.slider_scale = wx.Slider(self, value=100, minValue=10, maxValue=200, style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS)
self.Bind(wx.EVT_SLIDER, self.onSlider)
controlsSizer.Add(self.slider_scale, proportion=1, flag=wx.EXPAND, border=0)
self.panel_controls.SetSizer(controlsSizer)
###
self.panel_video = wx.Panel(self)
self.panel_video.SetBackgroundStyle(wx.BG_STYLE_PAINT)
self.panel_video.Bind(wx.EVT_PAINT, self.on_paint)
###
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.panel_controls, proportion=1, flag=wx.EXPAND, border=0)
mainSizer.Add(self.panel_video, proportion=9, flag=wx.EXPAND, border=0)
self.SetSizer(mainSizer)
self.Layout()
def initVideo(self):
self.vidCapDevice = cv2.VideoCapture()
self.vidCapDevice.open(0, apiPreference=cv2.CAP_V4L2)
requestCapWidth = 1024
requestCapHeight = 576
self.vidCapDevice.set(cv2.CAP_PROP_FRAME_WIDTH, requestCapWidth)
self.vidCapDevice.set(cv2.CAP_PROP_FRAME_HEIGHT, requestCapHeight)
self.capWidth = int(self.vidCapDevice.get(3))
self.capHeight = int(self.vidCapDevice.get(4))
self.vidCapDevice.set(cv2.CAP_PROP_FPS, 30)
self.latencyBuff_left = deque(maxlen=10)
self.latencyBuff_right = deque(maxlen=10)
self.stats_left = []
self.stats_right = []
self.textCoordsList_left = []
self.textCoordsList_right = []
self.statsInterval = 1000 # ms
self.statsTimer = 0
self.rightScale = 1.2
self.leftScale = 1.0 # from slider
def on_timer1(self, event):
self.panel_video.Refresh() # indirectly calls on_paint()
def on_paint(self, event):
self.updateVideoPanel()
def onSlider(self, event):
# We want a range of 0.1 - 2.0 but sliders only use ints,
# so set slider range to 10 - 200 and scale it by 0.01
obj = event.GetEventObject()
sliderVal = obj.GetValue()
self.leftScale = 0.01 * sliderVal
def updateVideoPanel(self):
ret, capture = self.vidCapDevice.read() # blocks until result
capture = cv2.cvtColor(capture, cv2.COLOR_BGR2RGB).astype(np.uint8)
bitmap = wx.Bitmap.FromBuffer(capture.shape[1], capture.shape[0], capture)
dc = wx.AutoBufferedPaintDC(self.panel_video)
gc = wx.GraphicsContext.Create(dc)
targetPanelWidth, targetPanelHeight = self.panel_video.GetSize()
gc.SetBrush(self.background_brush)
gc.DrawRectangle(0, 0, targetPanelWidth, targetPanelHeight)
bitmapWidth = bitmap.GetWidth()
bitmapHeight = bitmap.GetHeight()
leftWidth = int(bitmapWidth * self.leftScale)
leftHeight = int(bitmapHeight * self.leftScale)
rightWidth = int(bitmapWidth * self.rightScale)
rightHeight = int(bitmapHeight * self.rightScale)
left_padding = 10
top_padding = 10
gapWidth = 10
self.drawBitmap_gc(gc, bitmap,
left_padding,
top_padding,
leftWidth,
leftHeight,
self.latencyBuff_left)
self.drawBitmap_gc(gc, bitmap,
left_padding + leftWidth + gapWidth,
top_padding,
rightWidth,
rightHeight,
self.latencyBuff_right)
dc.SetTextForeground(wx.WHITE)
dc.SetFont((wx.Font(24, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)))
now = time_milli()
if now - self.statsTimer > self.statsInterval:
self.statsTimer = now
self.stats_left = []
self.stats_left += [str(f"capture size: {self.capWidth}x{self.capHeight}")]
self.stats_left += [str(f"output scale: {self.leftScale:.2f}")]
self.stats_left += [str(f"draw time: {np.average(self.latencyBuff_left):.1f}ms")]
self.stats_right = []
self.stats_right += [str(f"capture size: {self.capWidth}x{self.capHeight}")]
self.stats_right += [str(f"output scale: {self.rightScale:.2f}")]
self.stats_right += [str(f"draw time: {np.average(self.latencyBuff_right):.1f}ms")]
self.textCoordsList_left = []
for i, metric in enumerate(self.stats_left):
self.textCoordsList_left.append( tuple([left_padding + 10,
top_padding + (40 * i+1)]) )
self.textCoordsList_right = []
for i, metric in enumerate(self.stats_right):
self.textCoordsList_right.append( tuple([leftWidth + gapWidth + left_padding + 10,
top_padding + (40 * i+1)]) )
dc.DrawTextList(self.stats_left, self.textCoordsList_left)
dc.DrawTextList(self.stats_right, self.textCoordsList_right)
def drawBitmap_gc(self, gc, bitmap, left_padding, top_padding, width, height, latencyBuff):
if self.firstRun:
gr = gc.GetRenderer()
print("WX renderer:", gr.GetType() , gr.GetVersion())
self.firstRun = False
now = time_milli()
gc.DrawBitmap(bitmap, left_padding, top_padding, width, height)
latencyBuff.append(time_milli() - now)
def time_milli():
return time.time() * 1000
def main():
print("Python:", sys.version)
print("WXPython:", wx.version())
app = wx.App()
frame = MainFrame()
frame.Show(True)
app.MainLoop()
if __name__ == "__main__":
main()
I also noticed that aside from this problem, whichever draw happens second is about twice as quick as the first. Maybe I’m supposed to be re-using my dc or gc?
thanks in advance for looking at this!
Edit:
updated to
WXPython: 4.2.1 gtk3 (phoenix) wxWidgets 3.2.2.1
and it’s still the same