Skip to content

Commit b694bb3

Browse files
authored
Merge pull request #50 from martinRenou/get_image_data
Add get_image_data method
2 parents ddd6f33 + 792f2c7 commit b694bb3

File tree

7 files changed

+184
-95
lines changed

7 files changed

+184
-95
lines changed

docs/source/basic_usage.rst

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,6 @@ The ``Canvas`` and ``MultiCanvas`` classes have a ``clear`` method which allows
4848
4949
canvas.clear()
5050
51-
Save Canvas to a file
52-
---------------------
53-
54-
You can dump the current ``Canvas`` or ``MultiCanvas`` image using the ``to_file`` method. You first need to specify that you want the image data to be synchronized between the front-end and the back-end setting the ``sync_image_data`` attribute to ``True``.
55-
56-
.. code:: Python
57-
58-
from ipycanvas import Canvas
59-
60-
canvas = Canvas(size=(200, 200), sync_image_data=True)
61-
62-
# Perform some drawings...
63-
64-
canvas.to_file('my_file.png')
65-
66-
Note that this won't work if executed in the same Notebook cell. Because the Canvas won't have drawn anything yet. If you want to put all your code in the same Notebook cell, you need to define a callback function that will be called when the Canvas is ready to be dumped to an image file.
67-
68-
.. code:: Python
69-
70-
from ipycanvas import Canvas
71-
72-
canvas = Canvas(size=(200, 200), sync_image_data=True)
73-
74-
# Perform some drawings...
75-
76-
def save_to_file(*args, **kwargs):
77-
canvas.to_file('my_file.png')
78-
79-
# Listen to changes on the ``image_data`` trait and call ``save_to_file`` when it changes.
80-
canvas.observe(save_to_file, 'image_data')
81-
8251
Optimizing drawings
8352
-------------------
8453

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ You can try ipycanvas, without the need of installing anything on your computer,
2828
drawing_shapes
2929
drawing_text
3030
drawing_images
31+
retrieve_images
3132
canvas_state
3233
transformations
3334
events

docs/source/retrieve_images.rst

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Retrieve Canvas image
2+
=====================
3+
4+
There are two methods for retrieving the canvas image:
5+
6+
- ``to_file(filename)``: Dumps the image data to a PNG file.
7+
- ``get_image_data(x=0, y=0, width=None, height=None)``: Get the image data as a NumPy array for a sub-portion of the Canvas.
8+
9+
By default, and in order to keep ipycanvas fast, the image state of the Canvas is not synchronized between the TypeScript front-end and the Python back-end. If you want to retrieve the image data from the Canvas, you first need to explicitly specify that you want the image to be synchronized by setting ``sync_image_data`` to ``True`` before doing any drawing, you can set ``sync_image_data`` back to ``False`` once you're done.
10+
11+
Save Canvas to a file
12+
---------------------
13+
14+
You can dump the current ``Canvas`` or ``MultiCanvas`` image to a PNG file using the ``to_file`` method.
15+
16+
.. code:: Python
17+
18+
from ipycanvas import Canvas
19+
20+
canvas = Canvas(size=(200, 200), sync_image_data=True)
21+
22+
# Perform some drawings...
23+
24+
canvas.to_file('my_file.png')
25+
26+
Note that this won't work if executed in the same Notebook cell. Because the Canvas won't have drawn anything yet. If you want to put all your code in the same Notebook cell, you need to define a callback function that will be called when the Canvas is ready to be dumped to an image file.
27+
28+
.. code:: Python
29+
30+
from ipycanvas import Canvas
31+
32+
canvas = Canvas(size=(200, 200), sync_image_data=True)
33+
34+
# Perform some drawings...
35+
36+
def save_to_file(*args, **kwargs):
37+
canvas.to_file('my_file.png')
38+
39+
# Listen to changes on the ``image_data`` trait and call ``save_to_file`` when it changes.
40+
canvas.observe(save_to_file, 'image_data')
41+
42+
Get image data as a NumPy array
43+
-------------------------------
44+
45+
You can get the image data of the ``Canvas`` or ``MultiCanvas`` using the ``get_image_data`` method.
46+
47+
.. code:: Python
48+
49+
from ipycanvas import Canvas
50+
51+
canvas = Canvas(size=(200, 200), sync_image_data=True)
52+
53+
# Perform some drawings...
54+
55+
arr1 = canvas.get_image_data() # Get the entire Canvas as a NumPy array
56+
arr2 = canvas.get_image_data(50, 10, 40, 60) # Get the subpart defined by the rectangle at position (x=50, y=10) and of size (width=40, height=60)
57+
58+
Note that this won't work if executed in the same Notebook cell. Because the Canvas won't have drawn anything yet. If you want to put all your code in the same Notebook cell, you need to define a callback function that will be called when the Canvas has image data.
59+
60+
.. code:: Python
61+
62+
from ipycanvas import Canvas
63+
64+
canvas = Canvas(size=(200, 200), sync_image_data=True)
65+
66+
# Perform some drawings...
67+
68+
def get_array(*args, **kwargs):
69+
arr = canvas.get_image_data()
70+
# Do something with arr
71+
72+
# Listen to changes on the ``image_data`` trait and call ``get_array`` when it changes.
73+
canvas.observe(get_array, 'image_data')

examples/plotting.ipynb

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,21 @@
304304
"plot"
305305
]
306306
},
307+
{
308+
"cell_type": "markdown",
309+
"metadata": {},
310+
"source": [
311+
"### You can retrieve the entire ``Canvas`` or a subpart of it using the ``get_image_data`` method"
312+
]
313+
},
307314
{
308315
"cell_type": "code",
309316
"execution_count": null,
310317
"metadata": {},
311318
"outputs": [],
312319
"source": [
313-
"# Save the plot to an image file!\n",
314-
"plot.to_file('my_scatter.png')"
320+
"arr = plot.canvas.get_image_data(200, 300, 50, 100)\n",
321+
"arr.shape"
315322
]
316323
},
317324
{
@@ -320,16 +327,29 @@
320327
"metadata": {},
321328
"outputs": [],
322329
"source": [
323-
"from ipywidgets import Image\n",
330+
"plot.canvas[1].stroke_style = 'red'\n",
331+
"plot.canvas[1].line_width = 2\n",
332+
"plot.canvas[1].stroke_rect(200, 300, 50, 100)"
333+
]
334+
},
335+
{
336+
"cell_type": "code",
337+
"execution_count": null,
338+
"metadata": {},
339+
"outputs": [],
340+
"source": [
341+
"from ipycanvas import Canvas\n",
324342
"\n",
325-
"Image.from_file('my_scatter.png')"
343+
"c = Canvas(size=(50, 100))\n",
344+
"c.put_image_data(arr, 0, 0)\n",
345+
"c"
326346
]
327347
},
328348
{
329349
"cell_type": "markdown",
330350
"metadata": {},
331351
"source": [
332-
"### Because it's a Canvas, you can draw on top of it! "
352+
"### Or you can save it to a file using ``to_file``"
333353
]
334354
},
335355
{
@@ -338,9 +358,18 @@
338358
"metadata": {},
339359
"outputs": [],
340360
"source": [
341-
"plot[1].stroke_style = 'red'\n",
342-
"plot[1].line_width = 2\n",
343-
"plot[1].stroke_rect(200, 300, 50, 100)"
361+
"plot.canvas.to_file('my_scatter.png')"
362+
]
363+
},
364+
{
365+
"cell_type": "code",
366+
"execution_count": null,
367+
"metadata": {},
368+
"outputs": [],
369+
"source": [
370+
"from ipywidgets import Image\n",
371+
"\n",
372+
"Image.from_file('my_scatter.png')"
344373
]
345374
},
346375
{

ipycanvas/canvas.py

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,65 @@
1515

1616
from ._frontend import module_name, module_version
1717

18-
from .utils import binary_image, populate_args, to_camel_case
18+
from .utils import binary_image, populate_args, to_camel_case, image_bytes_to_array
1919

2020

21-
class Canvas(DOMWidget):
21+
class CanvasBase(DOMWidget):
22+
_model_module = Unicode(module_name).tag(sync=True)
23+
_model_module_version = Unicode(module_version).tag(sync=True)
24+
_view_module = Unicode(module_name).tag(sync=True)
25+
_view_module_version = Unicode(module_version).tag(sync=True)
26+
27+
size = Tuple((700, 500), help='Size of the Canvas, this is not equal to the size of the view').tag(sync=True)
28+
29+
#: (bool) Specifies if the image should be synchronized from front-end to Python back-end
30+
sync_image_data = Bool(False).tag(sync=True)
31+
32+
#: (bytes) Current image data as bytes (PNG encoded). It is ``None`` by default and will not be
33+
#: updated if ``sync_image_data`` is ``False``.
34+
image_data = Bytes(default_value=None, allow_none=True, read_only=True).tag(sync=True, **bytes_serialization)
35+
36+
def to_file(self, filename):
37+
"""Save the current Canvas image to a PNG file.
38+
39+
This will raise an exception if there is no image to save (_e.g._ if ``image_data`` is ``None``).
40+
"""
41+
if self.image_data is None:
42+
raise RuntimeError('No image data to save, please be sure that ``sync_image_data`` is set to True')
43+
if not filename.endswith('.png') and not filename.endswith('.PNG'):
44+
raise RuntimeError('Can only save to a PNG file')
45+
46+
with open(filename, 'wb') as fobj:
47+
fobj.write(self.image_data)
48+
49+
def get_image_data(self, x=0, y=0, width=None, height=None):
50+
"""Return a NumPy array representing the underlying pixel data for a specified portion of the canvas.
51+
52+
This will throw an error if there is no ``image_data`` to retrieve, this happens when nothing was drawn yet or
53+
when the ``sync_image_data`` attribute is not set to ``True``.
54+
The returned value is a NumPy array containing the image data for the rectangle of the canvas specified. The
55+
coordinates of the rectangle's top-left corner are (``x``, ``y``), while the coordinates of the bottom corner
56+
are (``x + width``, ``y + height``).
57+
"""
58+
if self.image_data is None:
59+
raise RuntimeError('No image data, please be sure that ``sync_image_data`` is set to True')
60+
61+
x = int(x)
62+
y = int(y)
63+
64+
if width is None:
65+
width = self.size[0] - x
66+
if height is None:
67+
height = self.size[1] - y
68+
69+
width = int(width)
70+
height = int(height)
71+
72+
image_data = image_bytes_to_array(self.image_data)
73+
return image_data[y:y + height, x:x + width]
74+
75+
76+
class Canvas(CanvasBase):
2277
"""Create a Canvas widget.
2378
2479
Args:
@@ -27,13 +82,7 @@ class Canvas(DOMWidget):
2782
"""
2883

2984
_model_name = Unicode('CanvasModel').tag(sync=True)
30-
_model_module = Unicode(module_name).tag(sync=True)
31-
_model_module_version = Unicode(module_version).tag(sync=True)
3285
_view_name = Unicode('CanvasView').tag(sync=True)
33-
_view_module = Unicode(module_name).tag(sync=True)
34-
_view_module_version = Unicode(module_version).tag(sync=True)
35-
36-
size = Tuple((700, 500), help='Size of the Canvas, this is not equal to the size of the view').tag(sync=True)
3786

3887
#: (valid HTML color) The color for filling rectangles and paths. Default to ``'black'``.
3988
fill_style = Color('black')
@@ -108,13 +157,6 @@ class Canvas(DOMWidget):
108157
#: (float) Specifies where to start a dash array on a line. Default is ``0.``.
109158
line_dash_offset = Float(0.)
110159

111-
#: (bool) Specifies if the image should be synchronized from front-end to Python back-end
112-
sync_image_data = Bool(False).tag(sync=True)
113-
114-
#: (bytes) Current image data as bytes (PNG encoded). It is ``None`` by default and will not be
115-
#: updated if ``sync_image_data`` is ``False``.
116-
image_data = Bytes(default_value=None, allow_none=True, read_only=True).tag(sync=True, **bytes_serialization)
117-
118160
_client_ready_callbacks = Instance(CallbackDispatcher, ())
119161
_mouse_move_callbacks = Instance(CallbackDispatcher, ())
120162
_mouse_down_callbacks = Instance(CallbackDispatcher, ())
@@ -347,7 +389,7 @@ def draw_image(self, image, x, y, width=None, height=None):
347389

348390
self._send_canvas_command('drawImage', (serialized_image, x, y, width, height))
349391

350-
def put_image_data(self, image_data, dx, dy):
392+
def put_image_data(self, image_data, dx=0, dy=0):
351393
"""Draw an image on the Canvas.
352394
353395
``image_data`` should be a NumPy array containing the image to draw and ``dx`` and ``dy`` the pixel position where to
@@ -440,19 +482,6 @@ def flush(self):
440482
self._commands_cache = []
441483
self._buffers_cache = []
442484

443-
def to_file(self, filename):
444-
"""Save the current Canvas image to a PNG file.
445-
446-
This will raise an exception if there is no image to save (_e.g._ if ``image_data`` is ``None``).
447-
"""
448-
if self.image_data is None:
449-
raise RuntimeError('No image data to save')
450-
if not filename.endswith('.png') and not filename.endswith('.PNG'):
451-
raise RuntimeError('Can only save to a PNG file')
452-
453-
with open(filename, 'wb') as fobj:
454-
fobj.write(self.image_data)
455-
456485
# Events
457486
def on_client_ready(self, callback, remove=False):
458487
"""Register a callback that will be called when a new client is ready to receive draw commands.
@@ -523,7 +552,7 @@ def _handle_frontend_event(self, _, content, buffers):
523552
self._mouse_out_callbacks(content['x'], content['y'])
524553

525554

526-
class MultiCanvas(DOMWidget):
555+
class MultiCanvas(CanvasBase):
527556
"""Create a MultiCanvas widget with n_canvases Canvas widgets.
528557
529558
Args:
@@ -532,20 +561,7 @@ class MultiCanvas(DOMWidget):
532561
"""
533562

534563
_model_name = Unicode('MultiCanvasModel').tag(sync=True)
535-
_model_module = Unicode(module_name).tag(sync=True)
536-
_model_module_version = Unicode(module_version).tag(sync=True)
537564
_view_name = Unicode('MultiCanvasView').tag(sync=True)
538-
_view_module = Unicode(module_name).tag(sync=True)
539-
_view_module_version = Unicode(module_version).tag(sync=True)
540-
541-
size = Tuple((700, 500), help='Size of the Canvas, this is not equal to the size of the view').tag(sync=True)
542-
543-
#: (bool) Specifies if the image should be synchronized from front-end to Python back-end
544-
sync_image_data = Bool(False).tag(sync=True)
545-
546-
#: (bytes) Current image data as bytes (PNG encoded). It is ``None`` by default and will not be
547-
#: updated if ``sync_image_data`` is ``False``.
548-
image_data = Bytes(default_value=None, allow_none=True, read_only=True).tag(sync=True, **bytes_serialization)
549565

550566
_canvases = List(Instance(Canvas)).tag(sync=True, **widget_serialization)
551567

@@ -592,19 +608,6 @@ def flush(self):
592608
for layer in self._canvases:
593609
layer.flush()
594610

595-
def to_file(self, filename):
596-
"""Save the current MultiCanvas image to a PNG file.
597-
598-
This will raise an exception if there is no image to save (_e.g._ if ``image_data`` is ``None``).
599-
"""
600-
if self.image_data is None:
601-
raise RuntimeError('No image data to save')
602-
if not filename.endswith('.png') and not filename.endswith('.PNG'):
603-
raise RuntimeError('Can only save to a PNG file')
604-
605-
with open(filename, 'wb') as fobj:
606-
fobj.write(self.image_data)
607-
608611

609612
@contextmanager
610613
def hold_canvas(canvas):

ipycanvas/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
"""Binary module."""
2+
from io import BytesIO
3+
4+
from PIL import Image as PILImage
5+
26
import numpy as np
37

48

9+
def image_bytes_to_array(im_bytes):
10+
"""Turn raw image bytes into a NumPy array."""
11+
im_file = BytesIO(im_bytes)
12+
13+
im = PILImage.open(im_file)
14+
15+
return np.array(im)
16+
17+
518
def binary_image(ar):
619
"""Turn a NumPy array representing an array of pixels into a binary buffer."""
720
if ar is None:

0 commit comments

Comments
 (0)