0

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?

Antony Hatchkins
  • 31,947
  • 10
  • 111
  • 111

1 Answers1

1

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.

bigreddot
  • 33,642
  • 5
  • 69
  • 122
  • Is it possible to have an `Instance` of a float or it must be wrapped into a `ColumnDataSource`? Is there a way to reuse image pixel data after the image is displayed without re-transferring it from python in a separate structure? – Antony Hatchkins Jun 14 '19 at 07:12
  • You don't need either, you can just have e.g. `width = Float(...)` I don't really understand the second question. You can put the image data in to one CDS that you share between the glyph and the tool, if that is what you are asking. – bigreddot Jun 14 '19 at 15:13
  • (2) From a [similar question](https://stackoverflow.com/questions/39986024/how-to-change-the-extent-and-position-of-an-existing-image-in-bokeh) I found out that `figure.image()` accepts `source` kwarg. In my case that would reduce the page size for the image would not have to be fed from python to js twice. I'd suggest including this kwarg it `bokeh.plotting.figure` [docs]( https://bokeh.pydata.org/en/latest/docs/reference/plotting.html#bokeh.plotting.figure.Figure.image). – Antony Hatchkins Jun 14 '19 at 20:03
  • (1) I've updated the code with `width = Float()`, but something is wrong it: "undefined property type for DrawTool.width" – Antony Hatchkins Jun 14 '19 at 20:13
  • On the JS side, it's actually `p.Number` not `p.Float`, Regarding adding `source` to `figure`, that's not feasible since a figure can have many glyphs, and each glyph can have a different source. – bigreddot Jun 14 '19 at 21:02
  • Yes, works with `p.Number`. Actually I didn't suggest adding `source` to `figure`, I suggested documenting already present `source` argument of `figure.image()` (see link above). Thank you for your responses, the new tool works as it should :) – Antony Hatchkins Jun 15 '19 at 18:22
  • @AntonyHatchkins it is in at least two places At the "Other Parameters" as the bottom of https://bokeh.pydata.org/en/latest/docs/reference/plotting.html#bokeh.plotting.figure.Figure.image specifically, and in the general sense of "all glyph methods accept a source" in https://bokeh.pydata.org/en/latest/docs/user_guide/data.html – bigreddot Jun 16 '19 at 01:39
  • Hmm. Yes, you're right, it is there in fact. Why didn't I notice it right away? Args that are explicitly specified in the signature of `figure.image()` are "Parameters", `kwargs` in the signature are "Keyword arguments". I stopped reading at that point. It was not immediately clear where "Other parameters" are supposed to go. But yes, formally it is there and I should've noticed it. – Antony Hatchkins Jun 16 '19 at 14:13
  • Docs are hard :) If you'd like to discuss ways to improve the information architecture of the site please feel free to start a topic on the Discourse https://discourse.bokeh.org/c/development – bigreddot Jun 16 '19 at 15:41
  • How about putting this tool into example apps docs section after I polish this solution until it is good enough for that (eg adding that red or inverse marker on the image)? – Antony Hatchkins Jun 16 '19 at 18:50
  • Would love to have a new example! A topic on the discourse would be a great place to discuss polishing it, etc! – bigreddot Jun 16 '19 at 19:18
  • I've created the topic: https://discourse.bokeh.org/t/custom-tool-plotting-a-vertical-line-profile-of-an-image/3606 – Antony Hatchkins Jun 17 '19 at 05:45