Python: Hacking Bokeh to create useful tools

Creating bounding box ground truth tool with Bokeh

Imports and user defined functions

In [11]:
from bokeh.models import CustomJS, ColumnDataSource, BoxSelectTool, Button, Range1d, Rect, ImageURL
from bokeh.plotting import figure, output_notebook, show, output_file
from bokeh.layouts import gridplot
from bokeh.resources import INLINE
from bokeh.embed import components
from jinja2 import Template
import os

def save_or_show_plot(layout, filename='', title=''):

    if filename:
        output_file(filename, title=title, mode='inline')
        show(layout)
    else:
        output_notebook(INLINE)
        show(layout)

Loading the data

In [12]:
#with open('imagelinks.txt') as f:
#    content = f.readlines()
img_list = ['https://raw.githubusercontent.com/ghandic/GroundTruth/master/Images/' + x for x in os.listdir('../Images')]


#img_list = [x.strip() for x in content]
source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))
out_source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))
img_source = ColumnDataSource(data=dict(url=img_list))
shown_img_source = ColumnDataSource(data=dict(x=[0], y=[1], width=[1], height=[1], url=[img_list[0]], counter=[0]))

Creating custom JavaScript callbacks

In [13]:
reset_callback = CustomJS(args=dict(source=source),
                           code="""
var data = source.data;
        
data['x'] = [];
data['y'] = [];
data['width'] = [];
data['height'] = [];
        
source.change.emit();
          
""")

download_callback = CustomJS(args=dict(out_source=out_source, img_source=img_source),
                           code="""
var outdata = out_source.data;
var img_data = img_source.data;
var filetext = "img,x,y,width,height\\n";

for (i=0; i < outdata['x'].length; i++) {
    
    if (outdata['x'][i]){
        var currRow = ['"' + img_data['url'][i].toString() + '"',
                    outdata['x'][i].toString(),
                    outdata['y'][i].toString(),
                    outdata['width'][i].toString(),
                    outdata['height'][i].toString().concat('\\n')];

        var joined = currRow.join();
        filetext = filetext.concat(joined);
    }
}

var filename = 'data_result.csv';
var blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' });

if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, filename);
}

else {
    var link = document.createElement("a");
    link = document.createElement('a')
    link.href = URL.createObjectURL(blob);
    link.download = filename
    link.target = "_blank";
    link.style.visibility = 'hidden';
    link.dispatchEvent(new MouseEvent('click'))
}
""")

submit_callback = CustomJS(args=dict(source=source, out_source=out_source, 
                                     img_source=img_source, shown_img_source=shown_img_source),
                           code="""
var data = source.data;
var outdata = out_source.data
var img_data = img_source.data;
var shown_img_data = shown_img_source.data;
var last_img = img_data['url'].length;

outdata['x'].push(data['x'][0]);
outdata['y'].push(data['y'][0]);
outdata['width'].push(data['width'][0]);
outdata['height'].push(data['height'][0]);

counter = Number(shown_img_data['counter']) + 1;
if (counter >= last_img) {

    var filetext = "img,x,y,width,height\\n";

    for (i=0; i < outdata['x'].length; i++) {
    
        if (outdata['x'][i]){
            var currRow = [img_data['url'][i].toString(),
                        outdata['x'][i].toString(),
                        outdata['y'][i].toString(),
                        outdata['width'][i].toString(),
                        outdata['height'][i].toString().concat('\\n')];

            var joined = currRow.join();
            filetext = filetext.concat(joined);
        }
    }

    var filename = 'bokeh_result.csv';
    var blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' });
    
    navigator.msSaveBlob(blob, filename);
    
    if (navigator.msSaveBlob) {
        navigator.msSaveBlob(blob, filename);
    }

    else {
        var link = document.createElement("a");
        link = document.createElement('a')
        link.href = URL.createObjectURL(blob);
        link.download = filename
        link.target = "_blank";
        link.style.visibility = 'hidden';
        link.dispatchEvent(new MouseEvent('click'))
    }
} else {

    console.log(img_data['url'][counter]);
    shown_img_data['url'] = [img_data['url'][counter]];
    shown_img_data['counter'] = counter
    shown_img_source.change.emit();
}

  
data['x'] = [];
data['y'] = [];
data['width'] = [];
data['height'] = [];
        
source.change.emit();
out_source.change.emit();
          
""")

get_box_callback = CustomJS(args=dict(source=source), code="""
// get data source from Callback args
var data = source.data;

/// get BoxSelectTool dimensions from cb_data parameter of Callback
console.log(cb_data)
var geometry = cb_data['geometry'];

/// calculate Rect attributes
var width = geometry['x1'] - geometry['x0'];
var height = geometry['y1'] - geometry['y0'];
var x = geometry['x0'] + width/2;
var y = geometry['y0'] + height/2;

/// update data source with new Rect attributes
data['x'] = [x];
data['y'] = [y];
data['width'] = [width];
data['height'] = [height];
        
// emit update of data source
source.change.emit();

""")

Creating the figure and widgets

In [14]:
box_select = BoxSelectTool(callback=get_box_callback)

p = figure(plot_width=600,
           plot_height=400,
           tools=[box_select, 'tap'],
           title="Select the cats!",
           x_range=Range1d(start=0.0, end=1.0),
           y_range=Range1d(start=0.0, end=1.0))

rect = Rect(x='x',
            y='y',
            width='width',
            height='height',
            fill_alpha=0.2,
            fill_color='#009933')

img = ImageURL(url='url', x='x', y='y', w='width', h='height')

p.add_glyph(shown_img_source, img)
p.add_glyph(source, rect, selection_glyph=rect)

p.js_on_event('tap', reset_callback)

download_button = Button(label="Download", callback = download_callback)
submit_button = Button(label="Submit", button_type="success", 
                       callback=submit_callback,width=580)

layout_rect = gridplot([[p], [submit_button], [download_button]])

p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
p.xaxis.visible = False
p.yaxis.visible = False
p.outline_line_color = None
p.title.align = "center"
p.title.text_font_size = '12pt'

save_or_show_plot(layout_rect)
Loading BokehJS ...

Centering the app for html page and removing toolbar

In [15]:
script, div = components(layout_rect)
js_resources = INLINE.render_js()
css_resources = INLINE.render_css()
newdiv = '<center>' + div +'\n</center>'

template = Template('''<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Boundingbox - Ground truth tool</title>
        {{ js_resources }}
        {{ css_resources }}
        {{ script }}
    <style>
    .wrapper {
        width: 800px;
        background-color: white;
        margin: 0 auto;
        }

    .plotdiv {
        margin: 0 auto;
        }
    .bk-toolbar-box {
        visibility: hidden;
    }
    </style>
    </head>
    <body>
    <div class='wrapper'>
        {{ div }}
    </div>
    </body>
</html>
''')

html = template.render(js_resources=js_resources,
                       css_resources=css_resources,
                       script=script,
                       div=newdiv)

Final output

In [16]:
with open('../Finished tools/Bokeh_ground_truth_tool.html', 'w') as file:
    file.write(html)
In [ ]: