Extract pixel values from px.imshow via clientside callback

Hi there,

I need to extract the pixel values from a px.imshow() with the argument binary_string=True via a client-side callback. I managed to this with a regular callback, but I really would prefer doing this client -side as the figure is quite big.

MRE:

from dash import Dash, html, dcc, Input, Output, State, clientside_callback
import plotly.express as px
import numpy as np
from PIL import Image
import base64
import io

app = Dash(
    __name__,
)

app.layout = html.Div([
    dcc.Graph(
        id='graph',
        figure=px.imshow(
            np.random.randint(0, 255, size=(50, 50)),
            binary_string=True
        )
    ),
    html.Button(id='btn_py', children=['python']),
    html.Button(id='btn_JS', children=['JS']),
    html.Div(id='out_py'),
    html.Div(id='out_JS'),
])


@app.callback(
    Output('out_py', 'children'),
    Input('btn_py', 'n_clicks'),
    State('graph', 'figure'),
    prevent_initial_call=True
)
def update(_, figure):
    full_string = figure.get('data')[0].get('source')
    _, image_string = full_string.split(',')
    image_bytes = base64.b64decode(image_string)
    image = Image.open(io.BytesIO(image_bytes))
    image_np = np.array(image)
    return str(image_np)


clientside_callback(
    """
    function(click, fig) {
        if (click == undefined) {
            throw window.dash_clientside.PreventUpdate;
        } else {
            // extract pixel values from figure
            const full_string = fig.data[0].source
            const image_string = full_string.split(',')[1]
            const decoded = atob(image_string)
            // const array_with_pixel_values = Uint8Array.from(decoded, c => c.charCodeAt(0))
            // return array_with_pixel_values
            // ^^ this does not work
            return "array_with_pixel_values"
        }
    }
    """,
    Output('out_JS', 'children'),
    Input('btn_JS', 'n_clicks'),
    State('graph', 'figure'),
    prevent_initial_call=True
)


if __name__ == '__main__':
    app.run(debug=True)

Hello @AIMPED,

Try this:

from dash import Dash, html, dcc, Input, Output, State, clientside_callback
import plotly.express as px
import numpy as np
# from PIL import Image
# import base64
# import io

app = Dash(
    __name__,
)

app.layout = html.Div([
    dcc.Graph(
        id='graph',
        figure=px.imshow(
            np.random.randint(0, 255, size=(50, 50)),
            binary_string=True
        )
    ),
    html.Button(id='btn_py', children=['python']),
    html.Button(id='btn_JS', children=['JS']),
    html.Div(id='out_py'),
    html.Div(id='out_JS'),
    html.Canvas(id='tempCanvas')
])


# @app.callback(
#     Output('out_py', 'children'),
#     Input('btn_py', 'n_clicks'),
#     State('graph', 'figure'),
#     prevent_initial_call=True
# )
# def update(_, figure):
#     full_string = figure.get('data')[0].get('source')
#     _, image_string = full_string.split(',')
#     image_bytes = base64.b64decode(image_string)
#     image = Image.open(io.BytesIO(image_bytes))
#     image_np = np.array(image)
#     return str(image_np)


clientside_callback(
    """
    function(click, fig) {
        if (click == undefined) {
            throw window.dash_clientside.PreventUpdate;
        } else {
            // extract pixel values from figure
            const canvas = document.getElementById("tempCanvas")
            const ctx = canvas.getContext("2d")
            var image = new Image();
            image.src = fig.data[0].source
            ctx.drawImage(image, 0, 0);
            var data = []
            for (var y = 0; y < canvas.getBoundingClientRect().height; y++) {
                for (var x = 0; x < canvas.getBoundingClientRect().width; x++) {
                    data.push(ctx.getImageData(x, y, 1, 1))
                }
            }
            console.log(data)
            return "array_with_pixel_values"
        }
    }
    """,
    Output('out_JS', 'children'),
    Input('btn_JS', 'n_clicks'),
    State('graph', 'figure'),
    prevent_initial_call=True
)


if __name__ == '__main__':
    app.run(debug=True)

I couldnt get PIL to work, so I commented it out. This will take the the image and convert it into a canvas, then pull the data from it.

Not sure how you want to pull the rest of the data. XD

1 Like

Hi @jinnyzor,

I really thought that this time I’d be closer to a working JS code with my initial way of doing it- I was wrong :rofl:

Unfortunately this throws

TypeError: Cannot read properties of null (reading 'getContext')

I suppose that this is due to not having an ID tempCanvas in the document? If I switch it for graph it throws the error

TypeError: canvas.getContext is not a function

Yes, you have to add the tempCanvas.

I tried straight from the graph itself as well.

This took me a while, but I think I figured out a way to do this:

    function(click, fig) {
        if (click == undefined) {
            throw window.dash_clientside.PreventUpdate;
        } else {
            // extract pixel values from figure
            var image = new Image();
            image.src = fig.data[0].source
            
            const canvas = document.createElement("canvas")
            canvas.width = image.width;
            canvas.height = image.height;
            
            const context = canvas.getContext("2d")
            context.drawImage(image, 0, 0);
            
            var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
            
            var data = []

            for (var y = 0; y < canvas.height; y++) {
                for (var x = 0; x < canvas.width; x++) {
                    var index = (y*imageData.width + x) * 4
                    data.push(imageData.data[index])
                }
            }
            return data
        }
    }

I was really confused why

ctx.getImageData(x, y, 1, 1)

returned four values, until I figured out, that these are the values of a RGBA Image. So in my case, I just need the value for red as my image is grayscale.

Helpful was this SO post:

1 Like

Like I said, I didn’t know how you wanted to handle the data. :grin:

1 Like

It’s all good, I did not expect an exact answer to my problem, but you pointed me perfectly into the right direction :hugs:

1 Like