jupyter-widgets-contrib / ipycanvas

Interactive Canvas in Jupyter
https://ipycanvas.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
686 stars 64 forks source link

Dynamic MultiCanvas #188

Open rsomani95 opened 3 years ago

rsomani95 commented 3 years ago

Thank you for this library!

I have a couple of questions regarding MultiCanvas objects.

  1. Is it safe to dynamically add canvases to a MultiCanvas object?
  2. Is this bad for performance? More specifically, is there a point beyond which performance regresses - say 10 canvases vs. 100 canvases?
from ipycanvas import MultiCanvas, Canvas

# (1)
canvas = MultiCanvas(n_canvases=1, width=100, height=100)
canvas._canvases.append(Canvas(width=100, height=100))
martinRenou commented 3 years ago

Is it safe to dynamically add canvases to a MultiCanvas object?

It should be safe, yes. Although you would be using private APIs, we should consider making them public. Note that I haven't tested it, but looking at the code I don't see why it wouldn't work.

Also note that canvas._canvases.append(Canvas()) wouldn't work, you need to re-assign the new list of canvases for Traitlets to realize there was a change, so something like canvas._canvases = canvas._canvases[:] + [Canvas()].

Also, please be sure that the Canvas you are adding has the same size (width and height) as the MultiCanvas, we should think of an API for adding new canvases that would do that automatically, any idea welcome!

Is this bad for performance? More specifically, is there a point beyond which performance regresses - say 10 canvases vs. 100 canvases?

I haven't tried it. You might reach performance issues around ~1000 Canvases?

martinRenou commented 3 years ago

Just out of curiosity, would you mind sharing your specific use-case?

The MultiCanvas is there for performance reasons, sometimes you want to move shapes on top of a static background that doesn't change at all. Then it makes sense to have two layers with a MultiCanvas, one for the static background and one for the moving shapes.

With a hundred layers you might reach the point where it does not make sense anymore to use MultiCanvas, and a single Canvas would be faster? (I am fully speculative here, I haven't tested it)

rsomani95 commented 3 years ago

Thanks for the thorough response @martinRenou

Also note that canvas._canvases.append(Canvas()) wouldn't work, you need to re-assign the new list of canvases for Traitlets to realize there was a change, so something like canvas._canvases = canvas._canvases[:] + [Canvas()].

Ah, I would've totally missed that. That makes sense.

Also, please be sure that the Canvas you are adding has the same size (width and height) as the MultiCanvas, we should think of an API for adding new canvases that would do that automatically, any idea welcome!

Here's a quick and dirty sketch of what I think could work (off the top of my head):

class MultiCanvas(...):
    ...
    def append_canvas(self, canvas:Canvas):
        if not canvas.height == self._canvases[0].height and canvas.width == self._canvases[0].width:
            raise ValueError(f"Expected height and width to be {....}"
        self._canvases = self._canvases[:] + [canvas]
rsomani95 commented 3 years ago

Just out of curiosity, would you mind sharing your specific use-case?

Sure! I'm trying to build a bounding box annotation tool for images. The setup is something like this:

The reason I thought dynamic MultiCanvas could work here is that very often, bounding boxes from different or same classes overlap, and perhaps if each box were on a separate canvas, selecting them and deleting or modifying them would be a more straightforward? For most use cases, anything above 100 boxes in an image is unrealistic.

Does that paint a clear picture? I realise this may be a bit abstract. I'm working on a sketch, and will be happy to share a notebook with you once I have something more concrete.