Search code examples
pythonimage-processingjupyter-notebookbokehbokehjs

Bokeh custom tool plotting a vertical line profile of an image


I need to plot a profile of an image, which is, to plot values of a matrix column.

And to implement it as a drag tool, which would automatically update the lower plot based on cursor position over the upper plot:

vertical line profile

Based on "A New Custom Tool" example from the docs I've written a code which works fine but has several problems:

import numpy as np
import bokeh.plotting as bp
from bokeh.models import CustomJS
from bokeh.layouts import layout, column, row

from bokeh.io import reset_output
from PIL import Image

im = Image.open(r'C:\Documents\image1.jpg')

z = np.array(im)[:,:,0]

from bokeh.core.properties import Instance, Float
from bokeh.io import output_file, show, output_notebook
from bokeh.models import ColumnDataSource, Tool
from bokeh.plotting import figure
from bokeh.util.compiler import TypeScript
from bokeh.layouts import layout, column, row

#output_file("a.html")
#reset_output()


# image vertical profile tool
TS_CODE = """
import {GestureTool, GestureToolView} from "models/tools/gestures/gesture_tool"
import {ColumnDataSource} from "models/sources/column_data_source"
import {GestureEvent} from "core/ui_events"
import * as p from "core/properties"

export class DrawToolView extends GestureToolView {
  model: DrawTool

  //this is executed when the pan/drag event starts
  _pan_start(_ev: GestureEvent): void {
    this.model.source.data = {x: [], y: []}
  }

  //this is executed on subsequent mouse/touch moves
  _pan(ev: GestureEvent): void {
    const {frame} = this.plot_view

    const {sx, sy} = ev
    if (!frame.bbox.contains(sx, sy))
      return

    const x = frame.xscales.default.invert(sx)
    const y = frame.yscales.default.invert(sy)

    var res = Array(128);
    var rx = Math.round(x);
    for(var i=0; i<128; i++) res[i] = this.model.zz.data["z"][i*225+rx];

    this.model.source.data = {
      x: Array(128).fill(0).map(Number.call, Number), 
      y: res
    };
    this.model.source.change.emit()
  }

  // this is executed then the pan/drag ends
  _pan_end(_ev: GestureEvent): void {}
}

export namespace DrawTool {
  export type Attrs = p.AttrsOf<Props>

  export type Props = GestureTool.Props & {
    source: p.Property<ColumnDataSource>,
    zz: p.Property<ColumnDataSource>,
    width: p.Float
  }
}

export interface DrawTool extends DrawTool.Attrs {}

export class DrawTool extends GestureTool {
  properties: DrawTool.Props

  constructor(attrs?: Partial<DrawTool.Attrs>) {
    super(attrs)
  }

  tool_name = "Drag Span"
  icon = "bk-tool-icon-lasso-select"
  event_type = "pan" as "pan"
  default_order = 12

  static initClass(): void {
    this.prototype.type = "DrawTool"
    this.prototype.default_view = DrawToolView

    this.define<DrawTool.Props>({
      source: [ p.Instance ],
      zz: [ p.Instance ],
      width: [ p.Float ]
    })
  }
}
DrawTool.initClass()
"""

class DrawTool(Tool):
    __implementation__ = TypeScript(TS_CODE)
    source = Instance(ColumnDataSource)
    zz = Instance(ColumnDataSource)
    width = Float()
output_notebook()

source = ColumnDataSource(data=dict(x=[], y=[]))
zz = ColumnDataSource(data=dict(z=z.flatten()))

p1 = figure(plot_width=600, plot_height=200, x_range=(0, 225), y_range=(0, 128), 
            tools=[DrawTool(source=source, zz=zz, width=225)])
im = p1.image(image=[np.flipud(z)], x=0, y=0, dw=225,
              dh=128, palette='Greys256')
p2 = figure(plot_width=600, plot_height=200)
p2.line('x', 'y', source=source)

bp.show(column(p1, p2))

1) Image dimensions are hard-coded now: how do I feed image dimensions from python to js?

2) The image is transferred to the client twice: first as an argument to image(), then as source for the button plot. How to access the image "source" from the DrawTool?

3) If (all this code being in one jupyter cell) I run it the second time it refuses to plot anything with a javascript error in console Model 'DrawTool' does not exist. Running it the third time, fourth time and further on works fine. What exactly is bokeh trying to tell me in this error message?


Solution

  • 1) Image dimensions are hard-coded now: how do I feed image dimensions from python to js?

    2) The image is transferred to the client twice: first as an argument to image(), then as source for the button plot. How to access the image "source" from the DrawTool?

    The answer to these is the same, add more properties (on both the Python and the JS sides) for the data you want to store on the DrawTool. E.g. another Instance for another ColumnDataSource that holds the image data, and integer properties for the width and height.

    3) If (all this code being in one jupyter cell) I run it the second time it refuses to plot anything with a javascript error in console Model 'DrawTool' does not exist. Running it the third time, fourth time and further on works fine. What exactly is bokeh trying to tell me in this error message?

    This message is stating that BokehJS does not know anything about any DrawTool, and the reason for this is that, due to the way things work in the notebook, custom extensions only get registered when you call output_notebook. So you will have to call output_notebook again after you define the custom extension. I don't like this state of affairs but there is nothing we can do about it.