Syncing a bokeh plot to a video
14 Sep 2019The Python library bokeh is great for plotting
all kinds of data in the browser. Bokeh includes a serve
command, which can
host a document, having a per-user state in the associated Python code. When
creating a visualisation, you can have callback functions in both Python and
JavaScript code. I used this here to sync an HTML video-element to a bokeh line
plot.
Accelerometer data synced to a dashcam video:
The HTML video element
<video src="bokeh-video-sync/static/20190912_041033_EF.mp4"
height="530"
id="frontcamera"></video>
The JavaScript callback:
/* Select the video with the id frontcamera */
var v = document.getElementById("frontcamera");
/* If the time of the video element updates, run the callback */
v.addEventListener("timeupdate", function () {
/* The selector below assumes there are no other input fields in your
code */
inputs = document.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
/* Update the input field with the new time of the video */
inputs[index].value = v.currentTime;
/* Trigger a change of the input field, to call the Python code */
var event = new Event('change', {bubbles: true});
inputs[index].dispatchEvent(event);
}
}, true);
The Python code, with the update callback function:
import pandas
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import curdoc, figure
def update(_, old, new):
"""The callback function we want to invoke if the time in the video
advances."""
# Our data source is in milliseconds, but the callback receives seconds,
# therefore we multiply the input by 1000
new, old = float(new) * 1000, float(old) * 1000
subset = df[(df['ms_since_start'] > old) & (df['ms_since_start'] < new)]
data.stream({
'ms': list(subset['ms_since_start']),
'z': list(subset['z_int']),
'x': list(subset['x_int']),
'y': list(subset['y_int']),
})
# Read in the data using pandas
df = pandas.read_csv('./20190912_041033_EF.acc.csv')
# Create a new bokeh figure
p = figure(plot_width=1900, plot_height=400)
# Define the columns to plot later
data = ColumnDataSource({
'ms': [],
'z': [],
'x': [],
'y': [],
})
# Plot three lines, that listen to changes in the ColumnDataSource
p.line('ms', 'z', source=data, color='red')
p.line('ms', 'x', source=data, color='white')
p.line('ms', 'y', source=data, color='lightgreen')
# Add a TextInput() that we use to pass on the current time in the video
# This is hacky, and could potentially be done in a nicer way
current_time = TextInput()
# If the text field current_time changes, invoke the update callback function
current_time.on_change('value', update)
# Add all elements to the bokeh document
curdoc().add_root(row(p))
curdoc().add_root(row(current_time))
Although I like this first try at syncing plots, there is still room for
improvement. It would be nicer to have the video element call the
Python function directly, instead of through an input field. This
could be achieved by implementing the video element in bokeh.models
.
Currently, the line will not disappear when you rewind the video, or start
it a second time. All these things can be achieved using the
HTML5 media events.
Maybe I’ll make a proper media player for bokeh one day…