Jpeg image lagging when zooming too much in FloatCanvas, NavCanvas Panning tool

When I use the NavCanvas Zooming / panning tool on a high res jpeg image at certain zoom level the app laggs the more you zoom and at times crashes completely leacing the following python error:

wx._core.wxAssertionError: C++ assertion ""data"" failed at ./src/common/image.cpp(535) in ResampleNearest(): unable to create image
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FloatCanvas.py", line 455, in WheelEvent
    self.GUIMode.OnWheel(event)
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/GUIMode.py", line 130, in OnWheel
    self.Canvas.Zoom(0.9, point, centerCoords = "Pixel", keepPointInPlace=True)
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FloatCanvas.py", line 761, in Zoom
    self.MoveImage(-delta, 'World')
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FloatCanvas.py", line 721, in MoveImage
    self.Draw()
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FloatCanvas.py", line 628, in Draw
    self._DrawObjects(dc, self._DrawList, ScreenDC, self.ViewPortBB, HTdc)
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FloatCanvas.py", line 988, in _DrawObjects
    Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc)
  File "/usr/lib/python3/dist-packages/wx/lib/floatcanvas/FCObjects.py", line 2164, in _Draw
    Img = self.Image.Scale(int(W), int(H))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
wx._core.wxAssertionError: C++ assertion ""(width > 0) && (height > 0)"" failed at ./src/common/image.cpp(449) in Scale(): invalid new image size

Using the following code:


import wx
from wx.lib.floatcanvas import FloatCanvas, NavCanvas
from wx.lib.floatcanvas.FCObjects import ScaledBitmap

class MyFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "ScrolledThumb Demo", size=(1600,800))

        path = "/home/user/images/my_shots/r5_manual_product_shots/1M4A0410.JPG"

        self.Canvas = NavCanvas.NavCanvas(self, -1,
                             # size=(500, 500),
                             ProjectionFun=None,
                             Debug=0,
                             BackgroundColor="White",
                             ).Canvas

        self.img = wx.Image(path, wx.BITMAP_TYPE_JPEG)
        
        W = self.img.GetWidth()
        H = self.img.GetHeight()

        self.bitmap = self.img

        self.fbitmap = ScaledBitmap(self.bitmap, (0,0), H)


        self.Canvas.AddObject(self.fbitmap)
# Add a circle
        cir = FloatCanvas.Circle((10, 10), 100)
        self.Canvas.AddObject(cir)

# Add a rectangle
        rect = FloatCanvas.Rectangle((110, 10), (100, 100), FillColor='Red')
        self.Canvas.AddObject(rect)

# Draw the canvas
        self.Canvas.Draw()


app = wx.App(False)
frame = MyFrame(None)
frame.Show(True)
app.MainLoop()

On Ubuntu 24.04

Any idea why and how to fix that?

It seems that ScaledBitmap scales the full image instead of just the part that needs to be painted:

Img = self.Image.Scale(int(W), int(H))

( line 2157 of Phoenix/wx/lib/floatcanvas/FCObjects.py at 4320e9da00bfc73ba3d92bb3a0f52a4ec6630f39 Ā· wxWidgets/Phoenix Ā· GitHub )

So, the memory usage increases with the zoom factor.

Sorry for being the lazy retart but do you have a solution to zoom in and out without loosing image details or have such high memory useage? In my case those are 8192*5464 images

  • Might not come in original size *

At a colour depth of 24 bit, your images result in bitmaps of 128MB.
With zoom factors well above 10 you can saturate most machines…

I doubt that anyone will fix this for you.

Are you willing to fix it yourself and submit a patch for wxPython?
You need to calculate which portion of the scaled bitmap will be visible, then the correlating portion of the original bitmap, scale up this portion and paint it, possibly shifted by some pixels.

Years ago I did something like this (not on floatcanvas), but I did only have integers as scaling factors.

Maybe you should write first what you actually want to accomplish with your software. Maybe floatcanvas is the wrong tool.

I don’t quite understand the image is ~134MB without compression (819254643) so why determining a smaller pourtion of that and just blow the BB / ROI over the whole screen with aliasing saturate a quite powerful i9 with GTX4070 and 64GB RAM machine?

What other widget is better for zooming panning, tilting and light editing of images?

Well, ScaledBitmap does not take a fraction and scale that. It scales the whole image:

self.Image.Scale(int(W), int(H))

I’m not aware of a widget that would work better.
I once started a class ImagePanel(wx.ScrolledWindow) but that would require a lot of fine-tuning and generalization. E.g. it does not work yet 100% when panning a zoomed image.


class ImagePanel(wx.ScrolledWindow):
    def init_data(self, frame, position="left"):
        self.frame = frame
        self.position = position
        self.img = self.bmp = self.img_size = None
        self.zoom = self._zoom = 1.0  # zoom can be None; _zoom is the calculated zoom value in this case
        self.SetScrollRate(10, 10)
        ...
        self.Bind(wx.EVT_PAINT, self.on_paint)
        ...

    def _draw_bmp(self, dc):
        # XXX scale and draw on the fly
        if self.img is None: return  # or self.bmp is None: return
        scale = self.zoom or self._calc_fit_zoom()  # XXX is None in case of fit
        scaled_size = self.img_size * scale
        size = self.Size

        if self.bmp and self.bmp.Size==scaled_size:
            print("Drawing scaled bitmap", self.GetVirtualSize())
            # if window is larger than scaled image -> add offset
            offset = (size-scaled_size)*0.5
            offset.IncTo((0,0))

            dc.DrawBitmap(self.bmp, offset.x, offset.y, False)
            return

        # calculate part of the image that would be plotted
        print("Scaling bitmap on the fly")

        # top left and size of (scaled) image, adjusted to multiples of scale
        TL = wx.Size( *self.CalcUnscrolledPosition((0,0)) )
        TL.DecBy( TL.x%scale, TL.y%scale )

        SIZE = wx.Size(size)
        SIZE.IncBy( scale - SIZE.width  % scale if SIZE.width%scale else 0,
                    scale - SIZE.height % scale if SIZE.height%scale else 0 )

        # extend by some margin for scrolling (not used yet)
        MARGIN = 150 # 0
        MARGIN_TL = wx.Size(MARGIN,MARGIN)  # top left
        MARGIN_TL.DecBy(MARGIN_TL.x%scale, MARGIN_TL.y%scale)  # adjust to multiples of scale

        # top left, size and bottom right of unscaled image part, including the margin
        POS_I = (TL - MARGIN_TL) * (1/scale)
        POS_I.IncTo( (0,0) )

        SIZE_I = (SIZE + MARGIN_TL) * (1/scale)

        POS_BR_I = POS_I + SIZE_I
        POS_BR_I.DecTo(self.img_size)

        # actually scale part BOX to SCALED_SIZE
        SCALED_SIZE = (POS_BR_I - POS_I) * scale
        BOX = (POS_I.x, POS_I.y, POS_BR_I.x, POS_BR_I.y)
        #scaled = self.img.resize(SCALED_SIZE, PIL.Image.BILINEAR, box=BOX)
        scaled = self.img.resize(SCALED_SIZE, PIL.Image.NEAREST, box=BOX)

        bmp = wx.Bitmap.FromBuffer( scaled.width, scaled.height, scaled.tobytes() )
        dc.DrawBitmap(bmp, TL.x-MARGIN_TL.x, TL.y-MARGIN_TL.y, False)


    def on_paint(self, event):
        dc = wx.PaintDC(self)
        self.DoPrepareDC(dc)
        self._draw_bmp(dc)
        if self._cursor_info:
            x,y,radius = self._cursor_info
            self._cursor_info = None
            self.draw_circles(dc, x,y,radius)
        ## Since we're not buffering in this case, we have to (re)paint the all the contents of the window, which can
        ## be potentially time consuming and flickery depending on what is being drawn and how much of it there is.
        #dc.Blit(xdest, ydest, width, height, source, xsrc, ysrc)        event.Skip()


    def on_size(self, event):
        # for now: just re-fit, if self.zoom is None
        if not self._setting_controls and self.img is not None and self.zoom is None:
            self.scale_image(None)
        event.Skip()
    def scale_image(self, factor, center=None):
        if not self.img: return

        if factor is None:
            # fit
            size = self.GetSize()
            scale = min( size.x/self.img_size.x, size.y/self.img_size.y)
        else:
            scale = factor
        self._zoom = scale

        if scale!=1.0:
            scaled_size = self.img_size * scale
            if factor is None or factor<=2.0 or False:  # XXX never scale XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                # actually scale
                scaled = self.img.resize(scaled_size, PIL.Image.BILINEAR)
            else:
                # scale only required parts on demand
                scaled = None
        else:
            scaled = self.img
            scaled_size = self.img_size
        if scaled is None:
            self.bmp = None
        elif self.bmp is None or self.bmp.Size!=scaled_size:
            self.bmp = wx.Bitmap.FromBuffer( scaled_size[0], scaled_size[1], scaled.tobytes() )
        else:
            # just overwrite buffer
            self.bmp.CopyFromBuffer(scaled.tobytes(), wx.BitmapBufferFormat_RGB)

        self._setting_controls = True
        self.SetVirtualSize( scaled_size )
        self._setting_controls = False

Yeah oviously it’d be quite dump to scale the whole image instead of just cutting the image roi and maximize that pourtion across the widget when zooming.

Are you sure there’s no function / windget / windows thing to deal with that?

I mean in the default image viewer in ubuntu gnome you can without a problem zoom 2000% into those high res images…

I’m sure that you can avoid the issue by setting like:

self.Canvas.MinScale=0.1
self.Canvas.MaxScale=10

I’d still like to have somewhat of an unlimited zoom, I don’t get why is that a problem wxpython isn’t a new module and this isn’t a complecated feathure if you know the source basics after all…

The reason was explained in the previous post.

Technically, I believe, the issue can be avoided. For example, in matplotlib, it doesn’t happen because the drawing area is restricted properly and by xlim and ylim. So you can keep zooming in ā€˜limitlessly’ until you reach the limits of machine epsilon.
I’m not sure if it can be fixed, but I have reported this problem in the issue tracker.

I doubt that anyone will address this.

If Mike does not want to take the code that I posted above, then matplotlib might be an option, but certainly not easier.
Anyway, there’s also example code for wxPython matplotlib integration, e.g. at wxGlade/examples/matplotlib3 at master Ā· wxGlade/wxGlade Ā· GitHub

Well I don’t think reling on matplotlib for such simple operation is good and it still realy bothers me that there is no proper solution for proper image viewing in this GUI framework…

I’d go deeper into the code to figure out why but at that stage wxpython might not be of that much use to me if it requires so much messing up with for such simple functions.

wxPython is a GUI toolkit, a meta toolkit unifying the API independent of which native GUI backend is being used.
I don’t think that any of the backends has what you want. It’s not their job.
The widgets are providing the basics. For what you want, you need to implement some kind of ā€œdocument windowā€.
Have a look at applications like maybe Word, Excel, Paint etc.
They are using the basic widgets for dialogs and everything around the document.
The actual document display is done by application specific code.
Such a ā€˜document’ could be matplotlib. Myself I’m very often using a different homegrown plotting library as well as matplotlib. For many ā€˜documents’ a grid is good enough.

The floatcanvas from wx.lib is a contribution providing another ā€˜document view’. It has it’s advantages. E.g. I think pure data plotting is faster than with matplotlib, which is quite heavy weight.

These contributed libraries implement something that someone sometime ago needed to implement and what he/she thought is useful for other people as well. You can’t expect that this code is 100% ready for any application. I’m quite sure that floatcanvas was not intended as image viewer.

I have an application where the user can easily check and assess X ray images.
It’s using Bitmap and it’s working fine if I limit the zoom factor. For this application the limit is not an issue. Anyway I disliked it, so I started the ImagePanel code above, but I did not go further than a ā€˜proof of concept’. I have the code in case I need it, but I did not have plans to develop it into production quality just for fun.

To a large degree, open source is based on ā€œeveryone scratchs his/her own itchā€. You have an itch, you may ignore it or scratch it.
I have provided you a starting point. I do not know your exact application so I can’t decide whether floatcanvas is the best base for your application or my ImagePanel or something completely different.

P.S.: The good thing about open source is that you can look at the sources and fix issues or see how to use the toolkit. In my experience that’s faster than using commercial tools which never have good enough documentation.

Maybe see the attached one. Not perfect, needs to be polished and possibly adapted to your needs.

Scaling the whole bitmap is a very bad idea - so the approach here is to scale the rendering DC (as wx.GraphicsContext).

I have tried to use wx.GraphicsRenderer.GetDirect2DRenderer() but it got pissed that it doesn’t like wx.AutoBufferedPaintDC() as an input. Maybe someone has more time than me to investigate.

On line 13, replace the image with whatever one you you have - I have used this one:

https://www.reddit.com/r/carporn/comments/1587gu2/my_little_1972_alfa_romeo_gtv_8192_x_5464/#lightbox

Andrea.

ImageViewer.py (6.0 KB)

3 Likes

@Andrea_Gavana - this works very nicely on my linux PC. The panning stays responsive and the memory usage is stable.

I still don’t know how to reverse engineer code proper quick and comftable enough especially without getting into very long rabbit holes and this feature just gives the impression that for feature and how it is well implemented across apps should be easy as having the full image in RAM and copying just a small pourting of it along side it for model and view display type system don’t sound like a complecated thing to do if you already know how to deal with the graphics data such with cropping rotating etc…

I’m typing to download the code but for somereason unable to.
may you please post it as a python code block?