<jupyter_start><jupyter_text>Draw Box an Canvas<jupyter_code>from ipycanvas import MultiCanvas

# Create a multi-layer canvas with 4 layers
multi_canvas = MultiCanvas(4, width=200, height=200)
multi_canvas[0] #  Access first layer (background)
multi_canvas[3] #  Access last layer
multi_canvas
# dynamic update of canvas size
multi_canvas.width = 30
multi_canvas.height = 60<jupyter_output><empty_output><jupyter_text>understand canvas grid![grid illustration](https://ipycanvas.readthedocs.io/en/latest/_images/grid.png)<jupyter_code>#exporti

def draw_bg(canvas, color='rgb(236,240,241)'):
    with hold_canvas(canvas):
        canvas.fill_style = color
        canvas.fill_rect(0, 0, canvas.size[0], canvas.size[1])
from ipycanvas import Canvas
canvas = Canvas(width=300, height=20)
draw_bg(canvas)
canvas
canvas = Canvas(width=200, height=200)
canvas.fill_rect(25, 25, 100, 100)
canvas.clear_rect(45, 45, 60, 60)
canvas.stroke_rect(50, 50, 50, 50)
canvas
#exporti

def draw_bounding_box(canvas, coords, color='white', line_width=None,
                      border_ratio=2, clear=False):
    with hold_canvas(canvas):
        if clear:
            canvas.clear()

        pos_x, pos_y, rect_x, rect_y = coords
        
        canvas.line_width = line_width or log(canvas.height)/5
        gap = canvas.line_width * border_ratio
        
        canvas.fill_style = color
        
        canvas.stroke_rect(pos_x, pos_y, rect_x, rect_y)
        canvas.fill_rect(pos_x, pos_y, rect_x, rect_y)
        canvas.stroke_rect(pos_x, pos_y, rect_x, rect_y)
        canvas.clear_rect(pos_x + gap, pos_y + gap, rect_x - 2 * gap,
                          rect_y - 2 * gap)
        canvas.stroke_rect(pos_x + gap, pos_y + gap, rect_x - 2 * gap,
                           rect_y - 2 * gap)
canvas = Canvas(width=100, height=100)
draw_bg(canvas)
draw_bounding_box(canvas, [0, 0, 80, 100])
draw_bounding_box(canvas, np.array([10, 40, 80, 100]) / 2, color='red', border_ratio=4, line_width=1.5)
canvas
canvas = MultiCanvas(2, width=200, height=200)
draw_bg(canvas[0])
draw_bounding_box(canvas[1], [10, 40, 80, 100])
draw_bounding_box(canvas[1], np.array([10, 40, 80, 100]) / 2, color='red', border_ratio=4)
canvas<jupyter_output><empty_output><jupyter_text>Draw Image- draw on canvas- canvas with bg- get image boundaries on canvas- get image scaling to retrieve org coordinateshttps://ipycanvas.readthedocs.io/en/latest/drawing_images.html<jupyter_code>from ipywidgets import Image

from ipycanvas import Canvas
from pathlib import Path

examples_path = Path("../data/projects/bbox/pics")
print(examples_path / "test200x200.png")
# sprite1 = Image.from_file(examples_path/"test200x200.png")

sprite1 = Image.from_file("../data/projects/bbox/pics/test200x200.png")
sprite2 = Image.from_file("../data/projects/bbox/pics/red400x640.png")

canvas = Canvas(width=300, height=300)

canvas.fill_style = '#a9cafc'
canvas.fill_rect(0, 0, 300, 300)

canvas.draw_image(sprite1, 50, 50)
canvas.draw_image(sprite2, 100, 100, width=40, height=40)

canvas<jupyter_output><empty_output><jupyter_text>You can draw from another Canvas widget. This is the fastest way of drawing an image on the canvas.<jupyter_code>canvas2 = Canvas(width=600, height=300)
# Here ``canvas`` is the canvas from the previous example
canvas2.draw_image(canvas, 0, 0)
canvas2.draw_image(canvas, 300, 0)
canvas2
#exporti
from PIL import Image as pilImage

# can we do this without reading image?
def get_image_size(path):
    pil_im = pilImage.open(path)
    return pil_im.width, pil_im.height
get_image_size("../data/projects/bbox/pics/red400x640.png")<jupyter_output><empty_output><jupyter_text>Draw resized Image- resize<jupyter_code>#exporti

def draw_img(canvas, file, clear=False):
    # draws resized image on canvas and returns scale used
    with hold_canvas(canvas):
        if clear:
            canvas.clear()

        sprite1 = Image.from_file(file)

        width_canvas, height_canvas = canvas.width, canvas.height
        width_img, height_img = get_image_size(file)

        ratio_canvas = float(width_canvas) / height_canvas
        ratio_img = float(width_img) / height_img

        if ratio_img > ratio_canvas:
            # wider then canvas, scale to canvas width
            scale = width_canvas / width_img
        else:
            # taller then canvas, scale to canvas hight
            scale = height_canvas / height_img

        canvas.draw_image(sprite1, 0, 0, 
                          width=width_img * min(1, scale),
                          height=height_img * min(1, scale))
        return scale
file = "../data/projects/bbox/pics/red400x640.png"
canvas = Canvas(width=300, height=300)
draw_bg(canvas)
scale = draw_img(canvas, file)
print(scale)
canvas
file = "../data/projects/bbox/pics/green640x400.png"
canvas = Canvas(width=300, height=300)
draw_bg(canvas)
scale = draw_img(canvas, file)
print(scale)
canvas<jupyter_output><empty_output><jupyter_text>Catch Drawing Eventsdrawing sequence:- mouse press- mouse move- mouse releaseonly watching on canvas area covered by image<jupyter_code>file = "../data/projects/bbox/pics/green640x400.png"
canvas = Canvas(width=300, height=300)
draw_bg(canvas)
scale = draw_img(canvas, file)
print(scale)
debug_view = widgets.Output(layout={'border': '1px solid black'})

state = {'active': False}

@debug_view.capture(clear_output=True)
def show_state():
    print(state)

@debug_view.capture(clear_output=True)
def update_start(x, y):
    print('start')
    state['active'] = True
    state['start'] = (x, y)
    # reset end
    state['end'] = state['start']
    show_state()

@debug_view.capture(clear_output=True)
def update_state(x, y):
    if state['active']:
        state['end'] = (x, y)
        show_state()
        
@debug_view.capture(clear_output=True)
def update_end(x, y):
    state['active'] = False
    state['end'] = (x, y)
    show_state()
        
canvas.on_mouse_down(update_start)
canvas.on_mouse_move(update_state)
canvas.on_mouse_up(update_end)
canvas
debug_view
a = Image.from_file("../data/projects/bbox/pics/test200x200.png")
#exporti

def points2bbox_coords(start_x, start_y, end_x, end_y):
    min_x, max_x = sorted((start_x, end_x)) 
    min_y, max_y = sorted((start_y, end_y))
    return {'x': min_x, 'y': min_y, 'width': max_x-min_x, 'height': max_y-min_y}<jupyter_output><empty_output><jupyter_text>Create BBoxCanvas ![](http://www.plantuml.com/plantuml/png/bP5DQiCm48NtEiL0DmdK5yWY1DehT2t6qZWnbJL6FtPo-rfIxQIj25JWXVK-FTwyEVc0BiJ125I17NnuSl0oN_f0Gh4DZWsyePtG2o6Is1pBxm46Zfv0ysp5sN4SuTtXtDkpA41pxDnYG5OVWAtCjCpqUlz8o9n3wcyBmpn2nmu8rGL6xXVCmS0J2LXEBSWcbcjeZd2ttrrVuRRFCfe0FYf5tdVHW4AVdfLJInP73MtBLwcOdVOLTisHmcSqCsGy0SS0t85AnD-dMyQr0Xwt1ZQBc3QIfErvRVfubv_KNwZ_PR1QfAzMpOIcrnW8P1bGFlPKx_bvRtdWYTmD_836mCwoRQRUzwsXVK7g5ieGMlWLj4NrkQuG0_tOucaHuc2-0000)<jupyter_code>#export

class BBoxCanvas(HBox, traitlets.HasTraits):
    """
    Represents canvas holding image and bbox ontop. 
    Gives user an ability to draw a bbox with mouse.
    
    """
    debug_output = widgets.Output(layout={'border': '1px solid black'})
    image_path = traitlets.Unicode()
    bbox_coords = traitlets.Dict()
    _canvas_bbox_coords = traitlets.Dict()
    _image_scale = traitlets.Float()
    
    def __init__(self, width, height):
        super().__init__()       

        self.is_drawing = False
        self._start_point = ()
        self._image_scale = 1.0
        
        self._bg_layer = 0
        self._image_layer = 1
        self._box_layer = 2
        # do not stick bbox to borders
        self.padding = 2

        # Define each of the children...
        self._image = Image(layout=Layout(display='flex',
                                          justify_content='center',
                                          align_items='center',
                                          align_content='center'))
        self._multi_canvas = MultiCanvas(3, width=width, height=height)
        self._im_name_box = Label()
            
        children = [VBox([self._multi_canvas, self._im_name_box])]
        self.children = children
        draw_bg(self._multi_canvas[self._bg_layer])
        
        # link drawing events        
        self._multi_canvas[self._box_layer].on_mouse_move(self._update_pos)
        self._multi_canvas[self._box_layer].on_mouse_down(self._start_drawing)
        self._multi_canvas[self._box_layer].on_mouse_up(self._stop_drawing)
        
    
    @debug_output.capture(clear_output=False)
    def _update_pos(self, x, y):
        if self.is_drawing:
            self._canvas_bbox_coords = points2bbox_coords(*self._start_point, x, y)
            # bbox should not cross the canvas border:
            if self._invalid_coords(x, y):
                print(' !! Out of canvas border !!')
                self._stop_drawing(x, y)
                    
    def _invalid_coords(self, x, y):
        return (self._canvas_bbox_coords["x"] + self._canvas_bbox_coords["width"] > self._multi_canvas.width - self.padding or
                self._canvas_bbox_coords["y"] + self._canvas_bbox_coords["height"] > self._multi_canvas.height - self.padding or
                self._canvas_bbox_coords["x"] < self.padding or
                self._canvas_bbox_coords["y"] < self.padding)
    
        
    @debug_output.capture(clear_output=True)            
    def _start_drawing(self, x, y):
#         print("-> START DRAWING")
        self._start_point = (x, y)
        self.is_drawing = True
#         print("<- START DRAWING")

    @debug_output.capture(clear_output=False)
    def _stop_drawing(self, x, y):
#         print("-> STOP DRAWING")
        self.is_drawing = False
        
        # if something is drawn
        if self._canvas_bbox_coords:
            # if bbox is not human visible, clean:
            if (self._canvas_bbox_coords['width'] < 10 or 
                self._canvas_bbox_coords['height'] < 10):
                self._canvas_bbox_coords = {}
                print(" !! too small bbox drawn !!")
            else: # otherwise, save bbox values to backend
                self.bbox_coords = dict({ k: v / self._image_scale for k, v in self._canvas_bbox_coords.items() })
#         print("<- STOP DRAWING")
        
        
    @traitlets.observe('bbox_coords')
    def _update_canvas_bbox_coords(self, change):
#         print('-> Observe bbox_coords: ', change)
        
        if change['new'] == self._canvas_bbox_coords: # change event from gui, do nothing
            pass
#             print('-> GUI')
        else: # recalculate canvas coordinates as bbox was set by backend:
            self._canvas_bbox_coords = {k: v * self._image_scale for k, v in self.bbox_coords.items()}
#             print('-> Backend')
#         print('<- Observe bbox_coords')
               
    
    @traitlets.observe('_canvas_bbox_coords')
    def _draw_bbox(self, change):
#         print('-> Observe canvas_coords: ', change)
        if not self._canvas_bbox_coords:
            self._clear_bbox()
            self.bbox_coords = {}
            return
        coords = [self._canvas_bbox_coords['x'],
                  self._canvas_bbox_coords['y'],
                  self._canvas_bbox_coords['width'],
                  self._canvas_bbox_coords['height']]
        draw_bounding_box(self._multi_canvas[self._box_layer], coords,
                          color='white', border_ratio=2, clear=True)
#         print('<- Observe canvas_coords')
        
        
    def _clear_bbox(self):
        self._multi_canvas[self._box_layer].clear()
    
        
    @traitlets.observe('image_path')
    def _draw_image(self, image):
        self._image_scale = draw_img(self._multi_canvas[self._image_layer], self.image_path, clear=True)
        self._im_name_box.value = Path(self.image_path).name

    @property
    def image_scale(self):
        return self._image_scale
    
    def _clear_image(self):
        self._multi_canvas[self._image_layer].clear()
    
    # needed to support voila
    # https://ipycanvas.readthedocs.io/en/latest/advanced.html#ipycanvas-in-voila
    def observe_client_ready(self, cb=None):
        self._multi_canvas.on_client_ready(cb)
gui = BBoxCanvas(width=100, height=100)
gui
gui.debug_output
gui.bbox_coords
# gui.bbox_coords = {'x':5, 'y': 5, 'width': 5, 'height': 5}
gui.bbox_coords = {}
# gui.image_path = "../data/projects/bbox/pics/red400x640.png"
gui.image_path = '../data/projects/im2im1/class_images/blocks_1.png'
gui.image_scale
# gui._draw_bbox([10, 20, 80, 100])
gui.bbox_coords = {'x': 5, 'y': 10, 'width': 20, 'height': 30}
gui.bbox_coords = {'x': 0, 'y': 10, 'width': 20, 'height': 30}
gui._clear_bbox()
#hide
from nbdev.export import *
notebook2script()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11635962840497559, 'fraction_numerical': 0.029916548575027554, 'mean_word_length': 3.1110032362459545, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 38, 'https://': 3, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '328893', 'license': 'Apache-2.0', 'n_tokens_mistral': 4688, 'n_tokens_neox': 4389, 'n_words': 839, 'path': 'nbs/01_bbox_canvas.ipynb', 'repo_name': 'EnriqueMoran/ipyannotator', 'size': '12702'}