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

does getting image from same cell work? #213

Closed Weihnachtshase closed 3 years ago

Weihnachtshase commented 3 years ago

I have tried to get an image of the canvas from the same cell as described in the documentation. I tried the following code:

from ipycanvas import Canvas
import numpy as np

canvas = Canvas(width=200, height=200, sync_image_data=True)
display(canvas)

def get_array(*args, **kwargs):
    print("here")
    arr = canvas.get_image_data()
    print(arr)
    np.save("whatever.txt", arr)
    # Do something with arr

# Listen to changes on the ``image_data`` trait and call ``get_array`` when it changes.
canvas.observe(get_array, 'image_data')

canvas.stroke_line(50, 10, 150, 10)

Unfortunately nothing happens (except the canvas being drawn). Can you confirm that this works for you?

PS: What I am trying to achieve is creating a gif/video from an animation, which is still an open issue as far as I have seen. But I try it "by hand".

martinRenou commented 3 years ago

Seems to work when I try locally. It does not print anything due to the callback (you could redirect the print statement to an output widget for example, see this).

The only thing I see is that it saves a whatever.txt.npy file. If you want to save to an image file you could still use canvas.to_file('my-image.png').

Closing as answered, but feel free to continue the discussion/reopen if you think otherwise.

Weihnachtshase commented 3 years ago

Thank you for your quick response!

Yes, I restarted the kernel and now it also creates the file for me (even though I also restarted the kernel earlier). The issue is therefore also closed for me.

The only problem that I'm currently facing is, that the same behaviour does not work for me, when the canvas is a property of another object. Do you maybe have a guess why that could be?

Here is a stripped down version of my problem:

from ipycanvas import Canvas
import numpy as np

def get_array(*args, **kwargs):
    print("here")
    arr = canvas.get_image_data()
    print(arr)
    np.save("whatever.txt", arr)
    # Do something with arr

class Test:

    def __init__(self):
        self.canvas = None

test = Test()
test.canvas = Canvas(width=200, height=200, sync_image_data=True)
display(test.canvas)
test.canvas.observe(get_array, 'image_data')
test.canvas.stroke_line(50, 10, 150, 10)
martinRenou commented 3 years ago

I cannot reproduce. It seems the callback is properly called for me.

Using this code:

from ipycanvas import Canvas
from ipywidgets import Output
import numpy as np

out = Output()

@out.capture()
def get_array(*args, **kwargs):
    print("saving")
    arr = canvas.get_image_data()

class Test:

    def __init__(self):
        self.canvas = None

display(out)
test = Test()
test.canvas = Canvas(width=200, height=200, sync_image_data=True)
display(test.canvas)
test.canvas.observe(get_array, 'image_data')
test.canvas.stroke_line(50, 10, 150, 10)

It always shows saving, so the callback is properly called

Weihnachtshase commented 3 years ago

Ok. Thank you for your help, I appreciate this very much!

martinRenou commented 3 years ago

Sure! How do you check if it works or not? The Jupyter Notebook file viewer does not seem to refresh at a necessary rate to see the whatever.txt.npy file appear

Weihnachtshase commented 3 years ago

So far I refresh manually every time. But what I'm trying to achieve is still not fully working, therefore manually refreshing every now and then is so far not a problem for me.

Weihnachtshase commented 3 years ago

Ok, maybe one last question then I'll stop bothering you.

from ipycanvas import Canvas, hold_canvas
import numpy as np
import time

class Test:

    def __init__(self):
        self.first = True
        self.images = []
        self.canvas = None

    def get_array(self, *args, **kwargs):
        arr = self.canvas.get_image_data()
        self.images.append(arr)

    def render(self, x):
        if self.first:
            self.canvas.observe(self.get_array, 'image_data')
            self.first = False
        with hold_canvas(self.canvas):
            self.canvas.stroke_line(50, 10+(x*5), 150, 10+(x*5))

test = Test()
canvas = Canvas(width=200, height=200, sync_image_data=True)
display(canvas)
test.canvas = canvas
for i in range(10):
    test.render(i)
    time.sleep(1)

This code shows a little animation of 10 lines that are displayed one after another. I would like to take an image (as a numpy array) after each individual image is rendered. However the callback function (get_array) is only called twice and not 10 times. Do you might know what the reason for that is?

martinRenou commented 3 years ago

Indeed... That's an annoying thing that happened to me when trying to make a prototype for the GIF animation creation.

I think it's because ipywidgets optimizes the communication and doesn't crowd the server with too many updates. It sees that the image_data property changes but it doesn't nudge the server as often. But I might be wrong about this.

It might also be due to your for-loop which stuck the Python kernel. What happens if you run this for-loop in a Python thread? (see the game of life example for a threading example).

Weihnachtshase commented 3 years ago

This was actually the problem :)

This code works now for me:

from ipycanvas import Canvas, hold_canvas
from threading import Thread
import numpy as np
import time

class Test:

    def __init__(self):
        self.first = True
        self.images = []
        self.canvas = None

    def get_array(self, *args, **kwargs):
        arr = self.canvas.get_image_data()
        self.images.append(arr)

    def render(self, x):
        if self.first:
            self.canvas.observe(self.get_array, 'image_data')
            self.first = False
        self.canvas.stroke_line(50, 10+(x*5), 150, 10+(x*5))

class MyThread(Thread):

    def __init__(self, test):
        super(MyThread, self).__init__()
        self.test = test

    def run(self):
        for i in range(10):
            self.test.render(i)
            time.sleep(1)

test = Test()
canvas = Canvas(width=200, height=200, sync_image_data=True)
display(canvas)
test.canvas = canvas
MyThread(test).start()

Hope that somehow helps you in the future with your GIF animation creation.

martinRenou commented 3 years ago

This is huge ahah. I haven't thought of threading when trying the first time.

Thanks a lot for trying! This will definitely help. We would need to think of a way to expose this as a nice and friendly user-API.